@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.
- package/README.md +46 -0
- package/dist/agent/runner.d.ts +117 -0
- package/dist/agent/runner.js +486 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +2 -0
- package/dist/chat-options.d.ts +37 -0
- package/dist/chat-options.js +42 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.js +201 -0
- package/dist/conversation/index.d.ts +17 -0
- package/dist/conversation/index.js +54 -0
- package/dist/engine/agent-engine.d.ts +38 -0
- package/dist/engine/agent-engine.js +146 -0
- package/dist/engine/index.d.ts +50 -0
- package/dist/engine/index.js +26 -0
- package/dist/engine/services.d.ts +20 -0
- package/dist/engine/services.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/profile.d.ts +57 -0
- package/dist/profile.js +211 -0
- package/dist/runtime/index.d.ts +23 -0
- package/dist/runtime/index.js +177 -0
- package/dist/update.d.ts +16 -0
- package/dist/update.js +74 -0
- package/dist/workbench/auth-controller.d.ts +43 -0
- package/dist/workbench/auth-controller.js +84 -0
- package/dist/workbench/auth-gate-controller.d.ts +62 -0
- package/dist/workbench/auth-gate-controller.js +231 -0
- package/dist/workbench/command-controller.d.ts +29 -0
- package/dist/workbench/command-controller.js +426 -0
- package/dist/workbench/conversation-controller.d.ts +32 -0
- package/dist/workbench/conversation-controller.js +53 -0
- package/dist/workbench/engine.d.ts +66 -0
- package/dist/workbench/engine.js +291 -0
- package/dist/workbench/input-controller.d.ts +44 -0
- package/dist/workbench/input-controller.js +71 -0
- package/dist/workbench/isolator-installer.d.ts +29 -0
- package/dist/workbench/isolator-installer.js +208 -0
- package/dist/workbench/lifecycle-controller.d.ts +30 -0
- package/dist/workbench/lifecycle-controller.js +75 -0
- package/dist/workbench/local-controller.d.ts +21 -0
- package/dist/workbench/local-controller.js +94 -0
- package/dist/workbench/render-model.d.ts +46 -0
- package/dist/workbench/render-model.js +61 -0
- package/dist/workbench/runtime-controller.d.ts +12 -0
- package/dist/workbench/runtime-controller.js +57 -0
- package/dist/workbench/session.d.ts +29 -0
- package/dist/workbench/session.js +42 -0
- package/dist/workbench/settings-controller.d.ts +80 -0
- package/dist/workbench/settings-controller.js +309 -0
- package/dist/workbench/shell-isolation.d.ts +20 -0
- package/dist/workbench/shell-isolation.js +13 -0
- package/dist/workbench/state.d.ts +187 -0
- package/dist/workbench/state.js +392 -0
- package/dist/workbench/turn-controller.d.ts +25 -0
- package/dist/workbench/turn-controller.js +164 -0
- package/dist/workbench/view-model.d.ts +34 -0
- package/dist/workbench/view-model.js +121 -0
- package/dist/workdir/index.d.ts +22 -0
- package/dist/workdir/index.js +46 -0
- package/package.json +50 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
export function createInitialWorkbenchState(options) {
|
|
2
|
+
const accessMode = options.accessMode ?? (options.contextEnabled ? "approval" : "off");
|
|
3
|
+
return {
|
|
4
|
+
messages: [
|
|
5
|
+
newMessage("system", "Agent API Workbench is ready. Type /help for commands."),
|
|
6
|
+
],
|
|
7
|
+
activities: [
|
|
8
|
+
newActivity("info", "Workbench started"),
|
|
9
|
+
],
|
|
10
|
+
busy: false,
|
|
11
|
+
contextEnabled: options.contextEnabled || accessMode === "approval" || accessMode === "full",
|
|
12
|
+
workdir: null,
|
|
13
|
+
activeAssistantMessageId: null,
|
|
14
|
+
pendingLocalTool: null,
|
|
15
|
+
accessMode,
|
|
16
|
+
currentConversation: options.conversation || "default",
|
|
17
|
+
runPreset: options.preset,
|
|
18
|
+
runModel: options.model,
|
|
19
|
+
renderMode: options.renderMode ?? "markdown",
|
|
20
|
+
defaultPreset: options.defaultPreset,
|
|
21
|
+
shellIsolation: options.shellIsolation,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function createInputHistory(limit = 100) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
let cursor = null;
|
|
27
|
+
let draftBeforeBrowse = "";
|
|
28
|
+
return {
|
|
29
|
+
record(value) {
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (!trimmed)
|
|
32
|
+
return;
|
|
33
|
+
if (entries.at(-1) !== trimmed) {
|
|
34
|
+
entries.push(trimmed);
|
|
35
|
+
if (entries.length > limit)
|
|
36
|
+
entries.splice(0, entries.length - limit);
|
|
37
|
+
}
|
|
38
|
+
cursor = null;
|
|
39
|
+
draftBeforeBrowse = "";
|
|
40
|
+
},
|
|
41
|
+
previous(currentDraft) {
|
|
42
|
+
if (entries.length === 0)
|
|
43
|
+
return currentDraft;
|
|
44
|
+
if (cursor === null) {
|
|
45
|
+
draftBeforeBrowse = currentDraft;
|
|
46
|
+
cursor = entries.length - 1;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
cursor = Math.max(0, cursor - 1);
|
|
50
|
+
}
|
|
51
|
+
return entries[cursor] ?? currentDraft;
|
|
52
|
+
},
|
|
53
|
+
next(currentDraft) {
|
|
54
|
+
if (entries.length === 0 || cursor === null)
|
|
55
|
+
return currentDraft;
|
|
56
|
+
if (cursor < entries.length - 1) {
|
|
57
|
+
cursor += 1;
|
|
58
|
+
return entries[cursor] ?? currentDraft;
|
|
59
|
+
}
|
|
60
|
+
cursor = null;
|
|
61
|
+
const restored = draftBeforeBrowse;
|
|
62
|
+
draftBeforeBrowse = "";
|
|
63
|
+
return restored;
|
|
64
|
+
},
|
|
65
|
+
reset() {
|
|
66
|
+
cursor = null;
|
|
67
|
+
draftBeforeBrowse = "";
|
|
68
|
+
},
|
|
69
|
+
values() {
|
|
70
|
+
return [...entries];
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function workbenchReducer(state, action) {
|
|
75
|
+
switch (action.type) {
|
|
76
|
+
case "message.add":
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
messages: [...state.messages, newMessage(action.role, action.text, action.id)],
|
|
80
|
+
};
|
|
81
|
+
case "message.append":
|
|
82
|
+
return {
|
|
83
|
+
...state,
|
|
84
|
+
messages: state.messages.map((message) => message.id === action.id ? { ...message, text: message.text + action.delta } : message),
|
|
85
|
+
};
|
|
86
|
+
case "messages.clear":
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
messages: [newMessage("system", "Cleared. Type /help for commands.")],
|
|
90
|
+
};
|
|
91
|
+
case "activity.add":
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
activities: [...state.activities, newActivity(action.level ?? "info", action.text)].slice(-20),
|
|
95
|
+
};
|
|
96
|
+
case "busy.set":
|
|
97
|
+
return { ...state, busy: action.busy };
|
|
98
|
+
case "context.toggle":
|
|
99
|
+
return setLocalAccess(state, state.contextEnabled ? "off" : "approval");
|
|
100
|
+
case "context.set":
|
|
101
|
+
return setLocalAccess(state, action.enabled ? "approval" : "off");
|
|
102
|
+
case "workdir.set":
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
workdir: action.workdir,
|
|
106
|
+
activities: [...state.activities, newActivity("success", `Workdir loaded: ${action.workdir.name}`)].slice(-20),
|
|
107
|
+
};
|
|
108
|
+
case "assistant.active":
|
|
109
|
+
return { ...state, activeAssistantMessageId: action.id };
|
|
110
|
+
case "local_tool.pending.set": {
|
|
111
|
+
const pending = {
|
|
112
|
+
...action.approval,
|
|
113
|
+
id: `local-${Date.now()}`,
|
|
114
|
+
createdAt: Date.now(),
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
...state,
|
|
118
|
+
pendingLocalTool: pending,
|
|
119
|
+
activities: [
|
|
120
|
+
...state.activities,
|
|
121
|
+
newActivity("warning", `Local approval ready: ${pending.name}${pending.action ? `.${pending.action}` : ""}`),
|
|
122
|
+
].slice(-20),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
case "local_tool.pending.clear":
|
|
126
|
+
return {
|
|
127
|
+
...state,
|
|
128
|
+
pendingLocalTool: null,
|
|
129
|
+
};
|
|
130
|
+
case "access.set":
|
|
131
|
+
return setLocalAccess(state, action.mode);
|
|
132
|
+
case "conversation.set":
|
|
133
|
+
return {
|
|
134
|
+
...state,
|
|
135
|
+
currentConversation: action.name,
|
|
136
|
+
activities: [...state.activities, newActivity("info", `Conversation: ${action.name}`)].slice(-20),
|
|
137
|
+
};
|
|
138
|
+
case "settings.set":
|
|
139
|
+
return {
|
|
140
|
+
...state,
|
|
141
|
+
...action.settings,
|
|
142
|
+
};
|
|
143
|
+
default:
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function setLocalAccess(state, mode) {
|
|
148
|
+
return {
|
|
149
|
+
...state,
|
|
150
|
+
accessMode: mode,
|
|
151
|
+
contextEnabled: mode !== "off",
|
|
152
|
+
pendingLocalTool: mode === "off" ? null : state.pendingLocalTool,
|
|
153
|
+
activities: [...state.activities, newActivity(mode === "off" ? "warning" : "success", `Local access: ${mode}`)].slice(-20),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function parseWorkbenchCommand(input) {
|
|
157
|
+
const trimmed = input.trim();
|
|
158
|
+
if (!trimmed.startsWith("/"))
|
|
159
|
+
return null;
|
|
160
|
+
const [name = "", ...rest] = trimmed.slice(1).split(/\s+/);
|
|
161
|
+
switch (name) {
|
|
162
|
+
case "abort":
|
|
163
|
+
case "cancel":
|
|
164
|
+
return { kind: "abort" };
|
|
165
|
+
case "quit":
|
|
166
|
+
return { kind: "quit" };
|
|
167
|
+
case "exit":
|
|
168
|
+
return { kind: "quit" };
|
|
169
|
+
case "help":
|
|
170
|
+
return { kind: "help" };
|
|
171
|
+
case "clear":
|
|
172
|
+
return { kind: "clear" };
|
|
173
|
+
case "login":
|
|
174
|
+
case "signin":
|
|
175
|
+
return { kind: "login" };
|
|
176
|
+
case "logout":
|
|
177
|
+
case "signout":
|
|
178
|
+
return { kind: "logout" };
|
|
179
|
+
case "delete-profile":
|
|
180
|
+
case "delete_profile":
|
|
181
|
+
return { kind: "delete_profile" };
|
|
182
|
+
case "switch-profile":
|
|
183
|
+
case "switch_profile":
|
|
184
|
+
return { kind: "switch_profile", name: rest.join(" ").trim() || undefined };
|
|
185
|
+
case "auth":
|
|
186
|
+
return { kind: "auth_status" };
|
|
187
|
+
case "config":
|
|
188
|
+
case "settings": {
|
|
189
|
+
const [field, ...valueParts] = rest;
|
|
190
|
+
if (!field)
|
|
191
|
+
return { kind: "config" };
|
|
192
|
+
if (field === "preset" || field === "isolation" || field === "isolator") {
|
|
193
|
+
return { kind: "config", field, value: valueParts.join(" ").trim() || undefined };
|
|
194
|
+
}
|
|
195
|
+
return { kind: "invalid", command: `${name} ${field}` };
|
|
196
|
+
}
|
|
197
|
+
case "render":
|
|
198
|
+
case "display":
|
|
199
|
+
case "view": {
|
|
200
|
+
const mode = rest[0];
|
|
201
|
+
if (mode === "raw" || mode === "markdown")
|
|
202
|
+
return { kind: "render", mode };
|
|
203
|
+
return { kind: "render" };
|
|
204
|
+
}
|
|
205
|
+
case "transcript":
|
|
206
|
+
return { kind: "transcript" };
|
|
207
|
+
case "export":
|
|
208
|
+
return { kind: "export", path: rest.join(" ").trim() || undefined };
|
|
209
|
+
case "context":
|
|
210
|
+
return { kind: "context", enabled: parseOnOff(rest[0]) };
|
|
211
|
+
case "access": {
|
|
212
|
+
const mode = rest[0];
|
|
213
|
+
if (mode === "off" || mode === "approval" || mode === "full")
|
|
214
|
+
return { kind: "access", mode };
|
|
215
|
+
return { kind: "access" };
|
|
216
|
+
}
|
|
217
|
+
case "preset": {
|
|
218
|
+
const value = rest.join(" ").trim();
|
|
219
|
+
return { kind: "preset", value: value || undefined };
|
|
220
|
+
}
|
|
221
|
+
case "model": {
|
|
222
|
+
const value = rest.join(" ").trim();
|
|
223
|
+
return { kind: "model", value: value || undefined };
|
|
224
|
+
}
|
|
225
|
+
case "workdir":
|
|
226
|
+
case "local":
|
|
227
|
+
return { kind: "workdir", enabled: parseOnOff(rest[0]) };
|
|
228
|
+
case "summary":
|
|
229
|
+
return { kind: "summary" };
|
|
230
|
+
case "new":
|
|
231
|
+
case "thread":
|
|
232
|
+
return { kind: "new_conversation", name: rest.join(" ").trim() || undefined };
|
|
233
|
+
case "conversation":
|
|
234
|
+
case "switch":
|
|
235
|
+
case "use":
|
|
236
|
+
if (rest.length === 0)
|
|
237
|
+
return { kind: "list_conversations" };
|
|
238
|
+
return { kind: "switch_conversation", name: rest.join(" ").trim() };
|
|
239
|
+
case "conversations":
|
|
240
|
+
case "threads":
|
|
241
|
+
return { kind: "list_conversations" };
|
|
242
|
+
case "refresh":
|
|
243
|
+
case "reload":
|
|
244
|
+
case "refresh-catalog":
|
|
245
|
+
return { kind: "refresh_catalog" };
|
|
246
|
+
case "search":
|
|
247
|
+
case "grep":
|
|
248
|
+
return { kind: "search", query: rest.join(" ").trim() };
|
|
249
|
+
case "preview":
|
|
250
|
+
return { kind: "preview" };
|
|
251
|
+
case "apply":
|
|
252
|
+
return { kind: "apply" };
|
|
253
|
+
case "apply-all":
|
|
254
|
+
case "yes-all":
|
|
255
|
+
return { kind: "apply_all" };
|
|
256
|
+
case "reject":
|
|
257
|
+
return { kind: "reject" };
|
|
258
|
+
default:
|
|
259
|
+
return { kind: "invalid", command: name };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function parsePendingApprovalCommand(input) {
|
|
263
|
+
const trimmed = input.trim().toLowerCase();
|
|
264
|
+
if (!trimmed.startsWith("/"))
|
|
265
|
+
return null;
|
|
266
|
+
const [name = ""] = trimmed.slice(1).split(/\s+/);
|
|
267
|
+
switch (name) {
|
|
268
|
+
case "apply":
|
|
269
|
+
case "yes":
|
|
270
|
+
return { kind: "apply" };
|
|
271
|
+
case "apply-all":
|
|
272
|
+
case "yes-all":
|
|
273
|
+
return { kind: "apply_all" };
|
|
274
|
+
case "reject":
|
|
275
|
+
case "no":
|
|
276
|
+
return { kind: "reject" };
|
|
277
|
+
default:
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export function helpText() {
|
|
282
|
+
return [
|
|
283
|
+
"Commands:",
|
|
284
|
+
"/auth show current auth profile",
|
|
285
|
+
"/login return to auth gate without deleting profiles",
|
|
286
|
+
"/logout leave current session and return to auth gate",
|
|
287
|
+
"/switch-profile switch/sign in with a different profile",
|
|
288
|
+
"/delete-profile delete current saved profile and return to auth",
|
|
289
|
+
"/config show current run configuration and saved defaults",
|
|
290
|
+
"/render [mode] show or set output rendering: markdown or raw",
|
|
291
|
+
"/transcript show a plain-text transcript preview",
|
|
292
|
+
"/export [file] save the plain-text transcript to a file",
|
|
293
|
+
"/config preset save default preset; use none/off for no preset, reset for built-in",
|
|
294
|
+
"/config isolation save shell isolation mode: none, auto, or required",
|
|
295
|
+
"/config isolator save agent-isolator path; use none/off to clear",
|
|
296
|
+
"/preset [name] show or set preset; use none/off to clear",
|
|
297
|
+
"/model [name] show or set explicit model; use auto/none/off to clear",
|
|
298
|
+
"/access [mode] show or set local tool access: off, approval, or full",
|
|
299
|
+
"/workdir show local workdir status",
|
|
300
|
+
"/workdir on shortcut for /access approval; /workdir off hides local tools",
|
|
301
|
+
"/new [name] start a fresh conversation in this workbench",
|
|
302
|
+
"/switch <name> switch to an existing/new conversation handle",
|
|
303
|
+
"/conversations list saved local conversation handles",
|
|
304
|
+
"/summary show local workdir summary previews",
|
|
305
|
+
"/search <query> search text in the local workdir",
|
|
306
|
+
"/preview show pending local action preview",
|
|
307
|
+
"/apply apply pending local action",
|
|
308
|
+
"/apply-all apply pending action and allow future local actions",
|
|
309
|
+
"/reject reject pending local action",
|
|
310
|
+
"/abort cancel the in-flight agent turn",
|
|
311
|
+
"/context toggle local context packaging for each agent turn",
|
|
312
|
+
"/clear clear the visible terminal transcript",
|
|
313
|
+
"/quit leave the workbench",
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
316
|
+
export function formatTranscript(messages) {
|
|
317
|
+
return messages
|
|
318
|
+
.map((message) => {
|
|
319
|
+
const body = message.text.trimEnd();
|
|
320
|
+
return body ? `${roleLabel(message.role)}:\n${body}` : `${roleLabel(message.role)}:`;
|
|
321
|
+
})
|
|
322
|
+
.join("\n\n")
|
|
323
|
+
.trimEnd() + "\n";
|
|
324
|
+
}
|
|
325
|
+
export function formatTranscriptPreview(messages, maxLines = 80) {
|
|
326
|
+
const lines = formatTranscript(messages).trimEnd().split(/\r?\n/);
|
|
327
|
+
if (lines.length <= maxLines) {
|
|
328
|
+
return ["Transcript preview:", "", ...lines].join("\n");
|
|
329
|
+
}
|
|
330
|
+
return [
|
|
331
|
+
`Transcript preview: showing last ${maxLines} lines of ${lines.length}. Use /export [file] for the full transcript.`,
|
|
332
|
+
"",
|
|
333
|
+
...lines.slice(-maxLines),
|
|
334
|
+
].join("\n");
|
|
335
|
+
}
|
|
336
|
+
function parseOnOff(value) {
|
|
337
|
+
if (!value)
|
|
338
|
+
return undefined;
|
|
339
|
+
const normalized = value.toLowerCase();
|
|
340
|
+
if (["on", "enable", "enabled", "yes", "true"].includes(normalized))
|
|
341
|
+
return true;
|
|
342
|
+
if (["off", "disable", "disabled", "no", "false"].includes(normalized))
|
|
343
|
+
return false;
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
export function workdirText(status) {
|
|
347
|
+
if (!status)
|
|
348
|
+
return "Workdir summary is still loading.";
|
|
349
|
+
return [
|
|
350
|
+
`Workdir: ${status.root}`,
|
|
351
|
+
`Files: ${status.fileCount}`,
|
|
352
|
+
`Size: ${formatBytes(status.totalBytes)}`,
|
|
353
|
+
`Scan truncated: ${status.scanTruncated ? "yes" : "no"}`,
|
|
354
|
+
].join("\n");
|
|
355
|
+
}
|
|
356
|
+
export function formatBytes(bytes) {
|
|
357
|
+
if (bytes < 1024)
|
|
358
|
+
return `${bytes} B`;
|
|
359
|
+
if (bytes < 1024 * 1024)
|
|
360
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
361
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
362
|
+
}
|
|
363
|
+
export function activityColor(level) {
|
|
364
|
+
if (level === "success")
|
|
365
|
+
return "green";
|
|
366
|
+
if (level === "warning")
|
|
367
|
+
return "yellow";
|
|
368
|
+
if (level === "error")
|
|
369
|
+
return "red";
|
|
370
|
+
return "gray";
|
|
371
|
+
}
|
|
372
|
+
function newMessage(role, text, id = randomId()) {
|
|
373
|
+
return { id, role, text };
|
|
374
|
+
}
|
|
375
|
+
function roleLabel(role) {
|
|
376
|
+
if (role === "user")
|
|
377
|
+
return "You";
|
|
378
|
+
if (role === "assistant")
|
|
379
|
+
return "Agent";
|
|
380
|
+
return "System";
|
|
381
|
+
}
|
|
382
|
+
function newActivity(level, text) {
|
|
383
|
+
return {
|
|
384
|
+
id: randomId(),
|
|
385
|
+
level,
|
|
386
|
+
text,
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function randomId() {
|
|
391
|
+
return Math.random().toString(36).slice(2);
|
|
392
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolveRuntimeProfile } from "../profile.js";
|
|
2
|
+
import { resumeAgentAfterLocalApproval, runAgentTurn, type AgentRunOptions, type LocalToolApprovalRequest, type WorkdirAccessMode } from "../agent.js";
|
|
3
|
+
import type { WorkbenchEngine, WorkbenchRuntimeEffect } from "./engine.js";
|
|
4
|
+
import type { WorkbenchAction, WorkbenchState } from "./state.js";
|
|
5
|
+
export interface WorkbenchTurnController {
|
|
6
|
+
startPrompt(prompt: string): Promise<void>;
|
|
7
|
+
continueAfterLocalApproval(input: {
|
|
8
|
+
approval: LocalToolApprovalRequest;
|
|
9
|
+
result: string | Record<string, unknown>;
|
|
10
|
+
accessMode: WorkdirAccessMode;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
abort(reason: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface WorkbenchTurnControllerOptions {
|
|
15
|
+
engine: WorkbenchEngine;
|
|
16
|
+
baseOptions: AgentRunOptions;
|
|
17
|
+
dispatch(action: WorkbenchAction): void;
|
|
18
|
+
getState(): WorkbenchState;
|
|
19
|
+
runRuntimeEffects(effects: WorkbenchRuntimeEffect[], assistantId: string): void;
|
|
20
|
+
flushTextDeltaBuffer(): void;
|
|
21
|
+
runAgentTurnImpl?: typeof runAgentTurn;
|
|
22
|
+
resumeAgentAfterLocalApprovalImpl?: typeof resumeAgentAfterLocalApproval;
|
|
23
|
+
resolveRuntimeProfileImpl?: typeof resolveRuntimeProfile;
|
|
24
|
+
}
|
|
25
|
+
export declare function createWorkbenchTurnController(options: WorkbenchTurnControllerOptions): WorkbenchTurnController;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { resolveRuntimeProfile, } from "../profile.js";
|
|
2
|
+
import { resumeAgentAfterLocalApproval, runAgentTurn, } from "../agent.js";
|
|
3
|
+
export function createWorkbenchTurnController(options) {
|
|
4
|
+
const runAgentTurnImpl = options.runAgentTurnImpl ?? runAgentTurn;
|
|
5
|
+
const resumeAgentAfterLocalApprovalImpl = options.resumeAgentAfterLocalApprovalImpl ?? resumeAgentAfterLocalApproval;
|
|
6
|
+
const resolveRuntimeProfileImpl = options.resolveRuntimeProfileImpl ?? resolveRuntimeProfile;
|
|
7
|
+
let activeAbortController = null;
|
|
8
|
+
let activeResponseID = null;
|
|
9
|
+
const cancelledResponseIDs = new Set();
|
|
10
|
+
return {
|
|
11
|
+
async startPrompt(prompt) {
|
|
12
|
+
const state = options.getState();
|
|
13
|
+
const assistantId = `assistant-${Date.now()}`;
|
|
14
|
+
const abortController = new AbortController();
|
|
15
|
+
activeAbortController = abortController;
|
|
16
|
+
activeResponseID = null;
|
|
17
|
+
options.dispatch({ type: "busy.set", busy: true });
|
|
18
|
+
options.dispatch({ type: "message.add", role: "user", text: prompt });
|
|
19
|
+
options.dispatch({ type: "message.add", role: "assistant", text: "", id: assistantId });
|
|
20
|
+
options.dispatch({ type: "assistant.active", id: assistantId });
|
|
21
|
+
options.dispatch({ type: "activity.add", text: "Agent turn started" });
|
|
22
|
+
try {
|
|
23
|
+
const result = await runAgentTurnImpl({
|
|
24
|
+
...options.baseOptions,
|
|
25
|
+
preset: state.runPreset,
|
|
26
|
+
model: state.runModel,
|
|
27
|
+
promptParts: [prompt],
|
|
28
|
+
stream: true,
|
|
29
|
+
file: undefined,
|
|
30
|
+
stdin: false,
|
|
31
|
+
conversation: state.currentConversation,
|
|
32
|
+
includeLocalContext: state.contextEnabled,
|
|
33
|
+
accessMode: state.accessMode,
|
|
34
|
+
shellIsolation: state.shellIsolation,
|
|
35
|
+
restartConversation: false,
|
|
36
|
+
abortSignal: abortController.signal,
|
|
37
|
+
}, (event) => handleAgentEvent(event, assistantId));
|
|
38
|
+
options.dispatch({
|
|
39
|
+
type: "activity.add",
|
|
40
|
+
level: "success",
|
|
41
|
+
text: result.responseID ? `Agent turn completed: ${result.responseID}` : "Agent turn completed",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
handleTurnError(error);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
finishTurn(abortController);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async continueAfterLocalApproval(input) {
|
|
52
|
+
const state = options.getState();
|
|
53
|
+
const assistantId = `assistant-${Date.now()}`;
|
|
54
|
+
const abortController = new AbortController();
|
|
55
|
+
activeAbortController = abortController;
|
|
56
|
+
activeResponseID = null;
|
|
57
|
+
options.dispatch({ type: "busy.set", busy: true });
|
|
58
|
+
options.dispatch({ type: "message.add", role: "assistant", text: "", id: assistantId });
|
|
59
|
+
options.dispatch({ type: "assistant.active", id: assistantId });
|
|
60
|
+
options.dispatch({ type: "activity.add", text: "Continuing agent turn" });
|
|
61
|
+
try {
|
|
62
|
+
const continuation = await resumeAgentAfterLocalApprovalImpl({
|
|
63
|
+
...options.baseOptions,
|
|
64
|
+
preset: state.runPreset,
|
|
65
|
+
model: state.runModel,
|
|
66
|
+
stream: true,
|
|
67
|
+
file: undefined,
|
|
68
|
+
stdin: false,
|
|
69
|
+
conversation: state.currentConversation,
|
|
70
|
+
includeLocalContext: state.contextEnabled,
|
|
71
|
+
accessMode: input.accessMode,
|
|
72
|
+
shellIsolation: state.shellIsolation,
|
|
73
|
+
restartConversation: false,
|
|
74
|
+
abortSignal: abortController.signal,
|
|
75
|
+
}, input.approval, input.result, (event) => handleAgentEvent(event, assistantId));
|
|
76
|
+
options.dispatch({
|
|
77
|
+
type: "activity.add",
|
|
78
|
+
level: "success",
|
|
79
|
+
text: continuation.responseID ? `Agent turn continued: ${continuation.responseID}` : "Agent turn continued",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
handleTurnError(error);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
finishTurn(abortController);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async abort(reason) {
|
|
90
|
+
const state = options.getState();
|
|
91
|
+
if (!state.busy && !activeAbortController && !activeResponseID) {
|
|
92
|
+
options.dispatch({ type: "message.add", role: "system", text: "No agent turn is running." });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
activeAbortController?.abort();
|
|
96
|
+
options.dispatch({ type: "activity.add", level: "warning", text: reason });
|
|
97
|
+
if (!activeResponseID) {
|
|
98
|
+
options.dispatch({ type: "message.add", role: "system", text: "Abort requested. No remote response ID is available yet." });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (cancelledResponseIDs.has(activeResponseID))
|
|
102
|
+
return;
|
|
103
|
+
const responseID = activeResponseID;
|
|
104
|
+
cancelledResponseIDs.add(responseID);
|
|
105
|
+
try {
|
|
106
|
+
const runtimeProfile = await resolveRuntimeProfileImpl(options.baseOptions.profile);
|
|
107
|
+
const result = await runtimeProfile.client.responses.cancel(responseID);
|
|
108
|
+
options.dispatch({
|
|
109
|
+
type: "message.add",
|
|
110
|
+
role: "system",
|
|
111
|
+
text: result.interrupted
|
|
112
|
+
? `Abort requested for response ${responseID}.`
|
|
113
|
+
: `Abort requested locally. Remote response ${responseID} was not actively interrupted.`,
|
|
114
|
+
});
|
|
115
|
+
options.dispatch({
|
|
116
|
+
type: "activity.add",
|
|
117
|
+
level: result.interrupted ? "success" : "warning",
|
|
118
|
+
text: result.interrupted ? `Response cancel requested: ${responseID}` : `Response was not active: ${responseID}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
options.dispatch({ type: "message.add", role: "system", text: `Abort requested locally, but remote cancel failed: ${userFacingError(error)}` });
|
|
123
|
+
options.dispatch({ type: "activity.add", level: "error", text: "Remote response cancel failed" });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
function handleAgentEvent(event, assistantId) {
|
|
128
|
+
const result = options.engine.handleAgentEvent(event);
|
|
129
|
+
for (const effect of result.effects) {
|
|
130
|
+
if (effect.type === "set_active_response_id") {
|
|
131
|
+
activeResponseID = effect.responseID;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
options.runRuntimeEffects(result.effects, assistantId);
|
|
135
|
+
}
|
|
136
|
+
function finishTurn(abortController) {
|
|
137
|
+
options.flushTextDeltaBuffer();
|
|
138
|
+
if (activeAbortController === abortController) {
|
|
139
|
+
activeAbortController = null;
|
|
140
|
+
}
|
|
141
|
+
activeResponseID = null;
|
|
142
|
+
options.dispatch({ type: "busy.set", busy: false });
|
|
143
|
+
options.dispatch({ type: "assistant.active", id: null });
|
|
144
|
+
}
|
|
145
|
+
function handleTurnError(error) {
|
|
146
|
+
const message = userFacingError(error);
|
|
147
|
+
const aborted = /aborted/i.test(message);
|
|
148
|
+
options.dispatch({
|
|
149
|
+
type: "message.add",
|
|
150
|
+
role: "system",
|
|
151
|
+
text: aborted ? "Agent turn aborted." : message,
|
|
152
|
+
});
|
|
153
|
+
options.dispatch({
|
|
154
|
+
type: "activity.add",
|
|
155
|
+
level: aborted ? "warning" : "error",
|
|
156
|
+
text: aborted ? "Agent turn aborted" : message,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function userFacingError(error) {
|
|
161
|
+
if (error instanceof Error)
|
|
162
|
+
return error.message;
|
|
163
|
+
return String(error);
|
|
164
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RenderMode, WorkbenchMessage } from "./state.js";
|
|
2
|
+
export type TranscriptLine = {
|
|
3
|
+
id: string;
|
|
4
|
+
text: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
bold?: boolean;
|
|
7
|
+
inverse?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export interface TranscriptViewModel {
|
|
10
|
+
lines: TranscriptLine[];
|
|
11
|
+
visibleLines: TranscriptLine[];
|
|
12
|
+
maxOffset: number;
|
|
13
|
+
offset: number;
|
|
14
|
+
viewportHeight: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildTranscriptViewModel(input: {
|
|
17
|
+
activeAssistantMessageId: string | null;
|
|
18
|
+
busy: boolean;
|
|
19
|
+
messages: WorkbenchMessage[];
|
|
20
|
+
offset: number;
|
|
21
|
+
renderMode: RenderMode;
|
|
22
|
+
spinnerFrame: number;
|
|
23
|
+
viewportHeight: number;
|
|
24
|
+
width: number;
|
|
25
|
+
}): TranscriptViewModel;
|
|
26
|
+
export declare function buildTranscriptLines(messages: WorkbenchMessage[], options: {
|
|
27
|
+
activeAssistantMessageId: string | null;
|
|
28
|
+
busy: boolean;
|
|
29
|
+
renderMode: RenderMode;
|
|
30
|
+
spinnerFrame: number;
|
|
31
|
+
width: number;
|
|
32
|
+
}): TranscriptLine[];
|
|
33
|
+
export declare function spinnerGlyph(frame: number): string;
|
|
34
|
+
export declare function elapsedDots(frame: number): string;
|