chainlesschain 0.45.11 → 0.45.19

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 (81) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/AppLayout-B00RARl2.js +1 -0
  3. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +1 -0
  4. package/src/assets/web-panel/assets/{Chat-5f__rMCR.js → Chat-DXtvKoM0.js} +1 -1
  5. package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-BJ4ODHOy.js} +1 -1
  6. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  7. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  8. package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-CSeKZEG_.js} +1 -1
  9. package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-BYQAK11r.js} +2 -2
  10. package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-gkUAPyuZ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-bjNrQgAo.js} +1 -1
  12. package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dbf57Tbv.js} +1 -1
  13. package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-CS0oMdxh.js} +1 -1
  14. package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-B2fgruv8.js} +1 -1
  15. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  16. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  17. package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
  18. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  19. package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-CF2CqPYX.js} +2 -2
  20. package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
  21. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  22. package/src/assets/web-panel/index.html +2 -2
  23. package/src/commands/agent.js +7 -8
  24. package/src/commands/chat.js +9 -11
  25. package/src/commands/serve.js +11 -106
  26. package/src/commands/session.js +185 -18
  27. package/src/commands/ui.js +10 -151
  28. package/src/gateways/repl/agent-repl.js +1 -0
  29. package/src/gateways/repl/chat-repl.js +1 -0
  30. package/src/gateways/ui/web-ui-server.js +1 -0
  31. package/src/gateways/ws/action-protocol.js +83 -0
  32. package/src/gateways/ws/message-dispatcher.js +73 -0
  33. package/src/gateways/ws/session-protocol.js +396 -0
  34. package/src/gateways/ws/task-protocol.js +55 -0
  35. package/src/gateways/ws/worktree-protocol.js +315 -0
  36. package/src/gateways/ws/ws-server.js +4 -0
  37. package/src/gateways/ws/ws-session-gateway.js +1 -0
  38. package/src/harness/background-task-manager.js +506 -0
  39. package/src/harness/background-task-worker.js +48 -0
  40. package/src/harness/compression-telemetry.js +214 -0
  41. package/src/harness/feature-flags.js +157 -0
  42. package/src/harness/jsonl-session-store.js +452 -0
  43. package/src/harness/prompt-compressor.js +416 -0
  44. package/src/harness/worktree-isolator.js +845 -0
  45. package/src/lib/agent-core.js +246 -45
  46. package/src/lib/background-task-manager.js +1 -305
  47. package/src/lib/background-task-worker.js +1 -50
  48. package/src/lib/compression-telemetry.js +5 -0
  49. package/src/lib/feature-flags.js +7 -182
  50. package/src/lib/interaction-adapter.js +32 -6
  51. package/src/lib/jsonl-session-store.js +21 -237
  52. package/src/lib/prompt-compressor.js +10 -351
  53. package/src/lib/sub-agent-context.js +91 -0
  54. package/src/lib/worktree-isolator.js +13 -231
  55. package/src/lib/ws-agent-handler.js +1 -0
  56. package/src/lib/ws-server.js +155 -359
  57. package/src/lib/ws-session-manager.js +82 -1
  58. package/src/repl/agent-repl.js +114 -32
  59. package/src/runtime/agent-runtime.js +417 -0
  60. package/src/runtime/contracts/agent-turn.js +11 -0
  61. package/src/runtime/contracts/session-record.js +31 -0
  62. package/src/runtime/contracts/task-record.js +18 -0
  63. package/src/runtime/contracts/telemetry-record.js +23 -0
  64. package/src/runtime/contracts/worktree-record.js +14 -0
  65. package/src/runtime/index.js +13 -0
  66. package/src/runtime/policies/agent-policy.js +45 -0
  67. package/src/runtime/runtime-context.js +14 -0
  68. package/src/runtime/runtime-events.js +37 -0
  69. package/src/runtime/runtime-factory.js +50 -0
  70. package/src/tools/index.js +22 -0
  71. package/src/tools/legacy-agent-tools.js +171 -0
  72. package/src/tools/registry.js +141 -0
  73. package/src/tools/tool-context.js +28 -0
  74. package/src/tools/tool-permissions.js +28 -0
  75. package/src/tools/tool-telemetry.js +39 -0
  76. package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
  77. package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
  78. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  79. package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +0 -3
  80. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  81. package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
@@ -1,305 +1 @@
1
- /**
2
- * Background Task Manager — daemon task queue with completion notifications.
3
- *
4
- * Tasks run in child_process.fork() for isolation.
5
- * Queue persisted to .chainlesschain/tasks/queue.jsonl.
6
- * Completion notifications delivered to REPL callback.
7
- *
8
- * Feature-flag gated: BACKGROUND_TASKS
9
- */
10
-
11
- import { fork } from "node:child_process";
12
- import {
13
- existsSync,
14
- mkdirSync,
15
- appendFileSync,
16
- readFileSync,
17
- writeFileSync,
18
- } from "node:fs";
19
- import { join } from "node:path";
20
- import { createHash } from "node:crypto";
21
- import { EventEmitter } from "node:events";
22
- import { getHomeDir } from "./paths.js";
23
-
24
- function getTasksDir() {
25
- const dir = join(getHomeDir(), "tasks");
26
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
27
- return dir;
28
- }
29
-
30
- function queuePath() {
31
- return join(getTasksDir(), "queue.jsonl");
32
- }
33
-
34
- // ── Task Status ─────────────────────────────────────────────────────────
35
-
36
- export const TaskStatus = {
37
- PENDING: "pending",
38
- RUNNING: "running",
39
- COMPLETED: "completed",
40
- FAILED: "failed",
41
- TIMEOUT: "timeout",
42
- };
43
-
44
- // ── BackgroundTaskManager ───────────────────────────────────────────────
45
-
46
- export class BackgroundTaskManager extends EventEmitter {
47
- /**
48
- * @param {object} options
49
- * @param {number} [options.maxConcurrent=3] — Max parallel background tasks
50
- * @param {number} [options.heartbeatTimeout=60000] — Task heartbeat timeout (ms)
51
- */
52
- constructor(options = {}) {
53
- super();
54
- this.maxConcurrent = options.maxConcurrent || 3;
55
- this.heartbeatTimeout = options.heartbeatTimeout || 60000;
56
- this.tasks = new Map(); // id -> task object
57
- this.processes = new Map(); // id -> child process
58
- this._checkInterval = null;
59
- }
60
-
61
- /**
62
- * Create and queue a new background task.
63
- * @param {object} spec
64
- * @param {string} spec.type — Task type (e.g. "shell", "agent", "script")
65
- * @param {string} spec.command — Command or script to execute
66
- * @param {string} [spec.cwd] — Working directory
67
- * @param {string} [spec.description] — Human-readable description
68
- * @returns {object} Created task
69
- */
70
- create(spec) {
71
- if (this._runningCount() >= this.maxConcurrent) {
72
- throw new Error(
73
- `Max concurrent tasks reached (${this.maxConcurrent}). Wait for a task to finish.`,
74
- );
75
- }
76
-
77
- const id = `task-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
78
-
79
- const task = {
80
- id,
81
- type: spec.type || "shell",
82
- command: spec.command,
83
- cwd: spec.cwd || process.cwd(),
84
- description: spec.description || spec.command,
85
- status: TaskStatus.PENDING,
86
- createdAt: Date.now(),
87
- startedAt: null,
88
- completedAt: null,
89
- lastHeartbeat: null,
90
- result: null,
91
- error: null,
92
- };
93
-
94
- this.tasks.set(id, task);
95
- this._persistTask(task);
96
- return task;
97
- }
98
-
99
- /**
100
- * Start a pending task (runs in child process).
101
- */
102
- start(taskId) {
103
- const task = this.tasks.get(taskId);
104
- if (!task) throw new Error(`Task not found: ${taskId}`);
105
- if (task.status !== TaskStatus.PENDING)
106
- throw new Error(`Task ${taskId} is not pending (status: ${task.status})`);
107
-
108
- task.status = TaskStatus.RUNNING;
109
- task.startedAt = Date.now();
110
- task.lastHeartbeat = Date.now();
111
-
112
- // Create a wrapper script that executes the command
113
- const child = fork(
114
- join(import.meta.dirname || ".", "background-task-worker.js"),
115
- [task.command, task.cwd, task.type],
116
- {
117
- cwd: task.cwd,
118
- silent: true,
119
- env: { ...process.env, CC_TASK_ID: taskId },
120
- },
121
- );
122
-
123
- this.processes.set(taskId, child);
124
-
125
- child.on("message", (msg) => {
126
- if (msg.type === "heartbeat") {
127
- task.lastHeartbeat = Date.now();
128
- } else if (msg.type === "result") {
129
- this._complete(taskId, TaskStatus.COMPLETED, msg.data, null);
130
- } else if (msg.type === "error") {
131
- this._complete(taskId, TaskStatus.FAILED, null, msg.error);
132
- }
133
- });
134
-
135
- child.on("exit", (code) => {
136
- if (task.status === TaskStatus.RUNNING) {
137
- if (code === 0) {
138
- this._complete(
139
- taskId,
140
- TaskStatus.COMPLETED,
141
- "Process exited (0)",
142
- null,
143
- );
144
- } else {
145
- this._complete(
146
- taskId,
147
- TaskStatus.FAILED,
148
- null,
149
- `Process exited with code ${code}`,
150
- );
151
- }
152
- }
153
- this.processes.delete(taskId);
154
- });
155
-
156
- child.on("error", (err) => {
157
- this._complete(taskId, TaskStatus.FAILED, null, err.message);
158
- this.processes.delete(taskId);
159
- });
160
-
161
- this._persistTask(task);
162
- this._ensureHeartbeatChecker();
163
- return task;
164
- }
165
-
166
- /**
167
- * Create and immediately start a task.
168
- */
169
- run(spec) {
170
- const task = this.create(spec);
171
- this.start(task.id);
172
- return task;
173
- }
174
-
175
- /**
176
- * Get a task by ID.
177
- */
178
- get(taskId) {
179
- return this.tasks.get(taskId) || null;
180
- }
181
-
182
- /**
183
- * List all tasks.
184
- */
185
- list(filter = {}) {
186
- let tasks = [...this.tasks.values()];
187
- if (filter.status) {
188
- tasks = tasks.filter((t) => t.status === filter.status);
189
- }
190
- return tasks.sort((a, b) => b.createdAt - a.createdAt);
191
- }
192
-
193
- /**
194
- * Stop a running task.
195
- */
196
- stop(taskId) {
197
- const child = this.processes.get(taskId);
198
- if (child) {
199
- child.kill("SIGTERM");
200
- setTimeout(() => {
201
- if (child.exitCode === null) child.kill("SIGKILL");
202
- }, 2000);
203
- }
204
- this._complete(taskId, TaskStatus.FAILED, null, "Stopped by user");
205
- }
206
-
207
- /**
208
- * Clean up completed/failed tasks older than maxAge.
209
- * @param {number} [maxAge=3600000] — Max age in ms (default 1 hour)
210
- */
211
- cleanup(maxAge = 3600000) {
212
- const cutoff = Date.now() - maxAge;
213
- let removed = 0;
214
- for (const [id, task] of this.tasks) {
215
- if (
216
- (task.status === TaskStatus.COMPLETED ||
217
- task.status === TaskStatus.FAILED ||
218
- task.status === TaskStatus.TIMEOUT) &&
219
- task.completedAt &&
220
- task.completedAt < cutoff
221
- ) {
222
- this.tasks.delete(id);
223
- removed++;
224
- }
225
- }
226
- return removed;
227
- }
228
-
229
- /**
230
- * Destroy the manager (kill all running tasks, clear intervals).
231
- */
232
- destroy() {
233
- if (this._checkInterval) {
234
- clearInterval(this._checkInterval);
235
- this._checkInterval = null;
236
- }
237
- for (const [id] of this.processes) {
238
- this.stop(id);
239
- }
240
- this.tasks.clear();
241
- this.processes.clear();
242
- }
243
-
244
- // ── Internal ──────────────────────────────────────────────────────
245
-
246
- _complete(taskId, status, result, error) {
247
- const task = this.tasks.get(taskId);
248
- if (!task) return;
249
-
250
- task.status = status;
251
- task.completedAt = Date.now();
252
- task.result = result;
253
- task.error = error;
254
-
255
- this._persistTask(task);
256
- this.emit("task:complete", task);
257
- }
258
-
259
- _runningCount() {
260
- let count = 0;
261
- for (const task of this.tasks.values()) {
262
- if (task.status === TaskStatus.RUNNING) count++;
263
- }
264
- return count;
265
- }
266
-
267
- _persistTask(task) {
268
- try {
269
- const line = JSON.stringify(task) + "\n";
270
- appendFileSync(queuePath(), line, "utf-8");
271
- } catch (_e) {
272
- // Non-critical
273
- }
274
- }
275
-
276
- _ensureHeartbeatChecker() {
277
- if (this._checkInterval) return;
278
-
279
- this._checkInterval = setInterval(
280
- () => {
281
- const now = Date.now();
282
- for (const [id, task] of this.tasks) {
283
- if (
284
- task.status === TaskStatus.RUNNING &&
285
- task.lastHeartbeat &&
286
- now - task.lastHeartbeat > this.heartbeatTimeout
287
- ) {
288
- this._complete(id, TaskStatus.TIMEOUT, null, "Heartbeat timeout");
289
- const child = this.processes.get(id);
290
- if (child) {
291
- child.kill("SIGKILL");
292
- this.processes.delete(id);
293
- }
294
- }
295
- }
296
- },
297
- Math.min(this.heartbeatTimeout / 2, 10000),
298
- );
299
-
300
- // Don't keep process alive for the checker
301
- if (this._checkInterval.unref) {
302
- this._checkInterval.unref();
303
- }
304
- }
305
- }
1
+ export { TaskStatus, BackgroundTaskManager } from "../harness/background-task-manager.js";
@@ -1,50 +1 @@
1
- /**
2
- * Background Task Worker — child process that executes a command.
3
- *
4
- * Args: [command, cwd, type]
5
- * Sends messages to parent: { type: "heartbeat"|"result"|"error", ... }
6
- */
7
-
8
- import { execSync } from "node:child_process";
9
-
10
- const [command, cwd, type] = process.argv.slice(2);
11
-
12
- // Heartbeat every 5 seconds
13
- const heartbeat = setInterval(() => {
14
- if (process.send) process.send({ type: "heartbeat" });
15
- }, 5000);
16
-
17
- try {
18
- let result;
19
-
20
- if (type === "shell") {
21
- result = execSync(command, {
22
- cwd: cwd || process.cwd(),
23
- encoding: "utf-8",
24
- timeout: 300000, // 5 min max
25
- maxBuffer: 10 * 1024 * 1024,
26
- });
27
- } else {
28
- // Default: treat as shell
29
- result = execSync(command, {
30
- cwd: cwd || process.cwd(),
31
- encoding: "utf-8",
32
- timeout: 300000,
33
- maxBuffer: 10 * 1024 * 1024,
34
- });
35
- }
36
-
37
- if (process.send) {
38
- process.send({ type: "result", data: result || "Done" });
39
- }
40
- } catch (err) {
41
- if (process.send) {
42
- process.send({
43
- type: "error",
44
- error: err.stderr || err.message || String(err),
45
- });
46
- }
47
- process.exitCode = 1;
48
- } finally {
49
- clearInterval(heartbeat);
50
- }
1
+ import "../harness/background-task-worker.js";
@@ -0,0 +1,5 @@
1
+ export {
2
+ recordCompressionMetric,
3
+ getCompressionTelemetrySummary,
4
+ resetCompressionTelemetry,
5
+ } from "../harness/compression-telemetry.js";
@@ -1,182 +1,7 @@
1
- /**
2
- * Feature Flag System — runtime feature gating for gradual rollout.
3
- *
4
- * Flags are stored in .chainlesschain/config.json under "features" key.
5
- * Each flag can be:
6
- * - boolean (true/false)
7
- * - number 0-100 (percentage rollout, hashed by machine-id)
8
- * - object { enabled, variant, description }
9
- *
10
- * Usage:
11
- * import { feature, featureVariant, listFeatures } from "./feature-flags.js";
12
- * if (feature("CONTEXT_SNIP")) { ... }
13
- */
14
-
15
- import { loadConfig, getConfigValue, saveConfig } from "./config-manager.js";
16
- import { createHash } from "node:crypto";
17
- import { hostname } from "node:os";
18
-
19
- // ── Flag Registry (source of truth for known flags) ────────────────────
20
-
21
- const FLAG_REGISTRY = {
22
- BACKGROUND_TASKS: {
23
- description: "Enable background task queue with daemon execution",
24
- default: false,
25
- },
26
- WORKTREE_ISOLATION: {
27
- description: "Enable git worktree isolation for agent tasks",
28
- default: false,
29
- },
30
- CONTEXT_SNIP: {
31
- description: "Enable snipCompact strategy in context compression",
32
- default: false,
33
- },
34
- CONTEXT_COLLAPSE: {
35
- description: "Enable contextCollapse strategy in context compression",
36
- default: false,
37
- },
38
- JSONL_SESSION: {
39
- description: "Use JSONL append-only format for session persistence",
40
- default: false,
41
- },
42
- PROMPT_COMPRESSOR: {
43
- description: "Enable CLI prompt compressor (auto/snip/collapse)",
44
- default: true,
45
- },
46
- };
47
-
48
- // ── Core API ────────────────────────────────────────────────────────────
49
-
50
- /**
51
- * Check if a feature flag is enabled.
52
- * @param {string} name - Flag name (e.g. "CONTEXT_SNIP")
53
- * @returns {boolean}
54
- */
55
- export function feature(name) {
56
- const value = _resolve(name);
57
- if (typeof value === "boolean") return value;
58
- if (typeof value === "number") return _percentageCheck(name, value);
59
- if (value && typeof value === "object") return Boolean(value.enabled);
60
- return _getDefault(name);
61
- }
62
-
63
- /**
64
- * Get the variant string for a feature (for A/B style flags).
65
- * @param {string} name
66
- * @returns {string|null}
67
- */
68
- export function featureVariant(name) {
69
- const value = _resolve(name);
70
- if (value && typeof value === "object" && value.variant) {
71
- return value.variant;
72
- }
73
- return null;
74
- }
75
-
76
- /**
77
- * List all known feature flags with their current values.
78
- * @returns {Array<{name, enabled, description, source, raw}>}
79
- */
80
- export function listFeatures() {
81
- const config = loadConfig();
82
- const features = config.features || {};
83
- const result = [];
84
-
85
- for (const [name, meta] of Object.entries(FLAG_REGISTRY)) {
86
- const raw = features[name];
87
- const enabled = feature(name);
88
- const source =
89
- raw !== undefined
90
- ? "config"
91
- : process.env[`CC_FLAG_${name}`] !== undefined
92
- ? "env"
93
- : "default";
94
-
95
- result.push({
96
- name,
97
- enabled,
98
- description: meta.description,
99
- source,
100
- raw: raw !== undefined ? raw : meta.default,
101
- });
102
- }
103
-
104
- // Include unknown flags from config (user-defined)
105
- for (const [name, raw] of Object.entries(features)) {
106
- if (!FLAG_REGISTRY[name]) {
107
- result.push({
108
- name,
109
- enabled: Boolean(raw),
110
- description: "(user-defined)",
111
- source: "config",
112
- raw,
113
- });
114
- }
115
- }
116
-
117
- return result;
118
- }
119
-
120
- /**
121
- * Set a feature flag value in config.
122
- * @param {string} name
123
- * @param {boolean|number|string} value
124
- */
125
- export function setFeature(name, value) {
126
- const config = loadConfig();
127
- if (!config.features) config.features = {};
128
- config.features[name] = value;
129
- saveConfig(config);
130
- }
131
-
132
- /**
133
- * Get the registry entry for a known flag (or null).
134
- * @param {string} name
135
- * @returns {{description: string, default: boolean}|null}
136
- */
137
- export function getFlagInfo(name) {
138
- return FLAG_REGISTRY[name] || null;
139
- }
140
-
141
- // ── Internal helpers ───────────────────────────────���────────────────────
142
-
143
- function _resolve(name) {
144
- // Priority: env var > config > default
145
- const envKey = `CC_FLAG_${name}`;
146
- if (process.env[envKey] !== undefined) {
147
- const envVal = process.env[envKey];
148
- if (envVal === "true" || envVal === "1") return true;
149
- if (envVal === "false" || envVal === "0") return false;
150
- const num = Number(envVal);
151
- if (!isNaN(num)) return num;
152
- return envVal;
153
- }
154
-
155
- const configVal = getConfigValue(`features.${name}`);
156
- if (configVal !== undefined) return configVal;
157
-
158
- return undefined; // let caller fall to default
159
- }
160
-
161
- function _getDefault(name) {
162
- const meta = FLAG_REGISTRY[name];
163
- return meta ? meta.default : false;
164
- }
165
-
166
- function _percentageCheck(name, percentage) {
167
- if (percentage <= 0) return false;
168
- if (percentage >= 100) return true;
169
- const hash = createHash("md5")
170
- .update(`${name}:${_machineId()}`)
171
- .digest("hex");
172
- const bucket = parseInt(hash.slice(0, 8), 16) % 100;
173
- return bucket < percentage;
174
- }
175
-
176
- let _cachedMachineId = null;
177
- function _machineId() {
178
- if (!_cachedMachineId) {
179
- _cachedMachineId = hostname() || "unknown";
180
- }
181
- return _cachedMachineId;
182
- }
1
+ export {
2
+ feature,
3
+ featureVariant,
4
+ listFeatures,
5
+ setFeature,
6
+ getFlagInfo,
7
+ } from "../harness/feature-flags.js";
@@ -101,18 +101,15 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
101
101
  * @param {object} [extra] - choices, default, etc.
102
102
  * @returns {Promise<string|boolean>}
103
103
  */
104
- _ask(questionType, question, extra = {}) {
104
+ _request(message, options = {}) {
105
105
  return new Promise((resolve, reject) => {
106
106
  const requestId = this._requestId();
107
107
  this._pending.set(requestId, { resolve, reject });
108
108
 
109
109
  this._sendWs({
110
- type: "question",
110
+ ...message,
111
111
  sessionId: this.sessionId,
112
112
  requestId,
113
- questionType,
114
- question,
115
- ...extra,
116
113
  });
117
114
 
118
115
  // Timeout after 5 minutes
@@ -123,11 +120,20 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
123
120
  reject(new Error("Question timed out"));
124
121
  }
125
122
  },
126
- 5 * 60 * 1000,
123
+ options.timeoutMs || 5 * 60 * 1000,
127
124
  );
128
125
  });
129
126
  }
130
127
 
128
+ _ask(questionType, question, extra = {}) {
129
+ return this._request({
130
+ type: "question",
131
+ questionType,
132
+ question,
133
+ ...extra,
134
+ });
135
+ }
136
+
131
137
  async askInput(question, options = {}) {
132
138
  return this._ask("input", question, { default: options.default || "" });
133
139
  }
@@ -157,6 +163,26 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
157
163
  }
158
164
  }
159
165
 
166
+ async requestHostTool(toolName, args = {}, extra = {}) {
167
+ return this._request(
168
+ {
169
+ type: "host-tool-call",
170
+ toolName,
171
+ args,
172
+ ...extra,
173
+ },
174
+ { timeoutMs: extra.timeoutMs || 60 * 1000 },
175
+ );
176
+ }
177
+
178
+ resolveHostTool(requestId, payload) {
179
+ const pending = this._pending.get(requestId);
180
+ if (pending) {
181
+ this._pending.delete(requestId);
182
+ pending.resolve(payload);
183
+ }
184
+ }
185
+
160
186
  emit(eventType, data) {
161
187
  this._sendWs({
162
188
  type: eventType,