chainlesschain 0.45.70 → 0.45.75

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 (89) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +1 -0
  4. package/src/assets/web-panel/assets/Analytics-sBrYoc3A.js +3 -0
  5. package/src/assets/web-panel/assets/AppLayout-2RCrdXxl.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-D9pBLPC3.css +1 -0
  7. package/src/assets/web-panel/assets/Backup-D68fenbD.js +1 -0
  8. package/src/assets/web-panel/assets/Backup-fZqtfC1m.css +1 -0
  9. package/src/assets/web-panel/assets/{Chat-DXtvKoM0.js → Chat-B2nB8o_F.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-BJ4ODHOy.js → Cron-CNs03iHJ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Dashboard-BZd4wDPQ.js → Dashboard-DanoHPSI.js} +2 -2
  12. package/src/assets/web-panel/assets/Git-CCMVr3Y8.js +2 -0
  13. package/src/assets/web-panel/assets/Git-DGcuBXST.css +1 -0
  14. package/src/assets/web-panel/assets/{Logs-CSeKZEG_.js → Logs-BY6A0UNG.js} +2 -2
  15. package/src/assets/web-panel/assets/{McpTools-BYQAK11r.js → McpTools-CrBVYlg6.js} +2 -2
  16. package/src/assets/web-panel/assets/{Memory-gkUAPyuZ.js → Memory-CWx3SpUt.js} +2 -2
  17. package/src/assets/web-panel/assets/{Notes-bjNrQgAo.js → Notes-1LcGD49x.js} +2 -2
  18. package/src/assets/web-panel/assets/Organization-DdOOM4ic.css +1 -0
  19. package/src/assets/web-panel/assets/Organization-Dx2DhbkM.js +4 -0
  20. package/src/assets/web-panel/assets/P2P-B16fjqfJ.js +2 -0
  21. package/src/assets/web-panel/assets/P2P-OEzOeMZX.css +1 -0
  22. package/src/assets/web-panel/assets/Permissions-BQbC9FzG.js +4 -0
  23. package/src/assets/web-panel/assets/Permissions-C9WlkGl-.css +1 -0
  24. package/src/assets/web-panel/assets/Projects-CjhZbNYm.js +2 -0
  25. package/src/assets/web-panel/assets/Projects-DxKelI5h.css +1 -0
  26. package/src/assets/web-panel/assets/Providers-BEakqcO5.css +1 -0
  27. package/src/assets/web-panel/assets/Providers-ivOAQtHM.js +2 -0
  28. package/src/assets/web-panel/assets/RssFeed-BlFC20eg.css +1 -0
  29. package/src/assets/web-panel/assets/RssFeed-BrsErdrU.js +3 -0
  30. package/src/assets/web-panel/assets/Security-DnEvJU5h.js +4 -0
  31. package/src/assets/web-panel/assets/Security-Dwxw7rfP.css +1 -0
  32. package/src/assets/web-panel/assets/{Services-CS0oMdxh.js → Services-7jQywNbl.js} +2 -2
  33. package/src/assets/web-panel/assets/Skills-CLlblJcG.js +1 -0
  34. package/src/assets/web-panel/assets/{Tasks-qULws8pc.js → Tasks-CmJBC1cf.js} +1 -1
  35. package/src/assets/web-panel/assets/Templates-DOY_oZnm.css +1 -0
  36. package/src/assets/web-panel/assets/Templates-RXT8-DNk.js +1 -0
  37. package/src/assets/web-panel/assets/Wallet-3iYASEx_.js +4 -0
  38. package/src/assets/web-panel/assets/Wallet-DnIumafl.css +1 -0
  39. package/src/assets/web-panel/assets/WebAuthn-CNPl2VQR.css +1 -0
  40. package/src/assets/web-panel/assets/WebAuthn-s3Hzd9db.js +5 -0
  41. package/src/assets/web-panel/assets/{antd-CJSBocer.js → antd-gZyc63Qr.js} +114 -114
  42. package/src/assets/web-panel/assets/chat-DWBA4-cl.js +1 -0
  43. package/src/assets/web-panel/assets/index-CyGtHm63.js +2 -0
  44. package/src/assets/web-panel/assets/{markdown-Bo5cVN4u.js → markdown-Bv7nG63L.js} +1 -1
  45. package/src/assets/web-panel/assets/ws-CU7Gvoom.js +1 -0
  46. package/src/assets/web-panel/index.html +2 -2
  47. package/src/commands/doctor.js +33 -151
  48. package/src/commands/mcp.js +1 -1
  49. package/src/commands/plugin.js +1 -1
  50. package/src/commands/session.js +106 -7
  51. package/src/commands/status.js +39 -69
  52. package/src/gateways/ws/session-protocol.js +1 -1
  53. package/src/gateways/ws/ws-agent-handler.js +484 -0
  54. package/src/gateways/ws/ws-server.js +758 -4
  55. package/src/gateways/ws/ws-session-gateway.js +1432 -1
  56. package/src/harness/mcp-client.js +417 -0
  57. package/src/harness/mock-llm-provider.js +167 -0
  58. package/src/harness/plugin-manager.js +434 -0
  59. package/src/lib/agent-core.js +25 -1902
  60. package/src/lib/hashline.js +208 -0
  61. package/src/lib/jsonl-session-store.js +11 -0
  62. package/src/lib/mcp-client.js +14 -412
  63. package/src/lib/plugin-manager.js +29 -428
  64. package/src/lib/prompt-compressor.js +11 -0
  65. package/src/lib/session-hooks.js +61 -0
  66. package/src/lib/skill-loader.js +4 -0
  67. package/src/lib/skill-mcp.js +190 -0
  68. package/src/lib/workflow-state-reader.js +94 -0
  69. package/src/lib/ws-agent-handler.js +8 -472
  70. package/src/lib/ws-server.js +12 -756
  71. package/src/lib/ws-session-manager.js +8 -1417
  72. package/src/repl/agent-repl.js +27 -3
  73. package/src/runtime/agent-core.js +1760 -0
  74. package/src/runtime/agent-runtime.js +3 -1
  75. package/src/runtime/coding-agent-contract-shared.cjs +496 -0
  76. package/src/runtime/coding-agent-contract.js +49 -229
  77. package/src/runtime/coding-agent-policy.cjs +54 -5
  78. package/src/runtime/diagnostics.js +317 -0
  79. package/src/runtime/index.js +3 -0
  80. package/src/tools/index.js +3 -0
  81. package/src/tools/legacy-agent-tools.js +5 -0
  82. package/src/assets/web-panel/assets/AppLayout-B_tkw3Pn.js +0 -1
  83. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +0 -1
  84. package/src/assets/web-panel/assets/Providers-Brm-S_hS.css +0 -1
  85. package/src/assets/web-panel/assets/Providers-Dbf57Tbv.js +0 -1
  86. package/src/assets/web-panel/assets/Skills-B2fgruv8.js +0 -1
  87. package/src/assets/web-panel/assets/chat-DnH09sSR.js +0 -1
  88. package/src/assets/web-panel/assets/index-IK-oro0g.js +0 -2
  89. package/src/assets/web-panel/assets/ws-DjelKkD6.js +0 -1
@@ -0,0 +1,484 @@
1
+ /**
2
+ * WebSocket Agent Handler
3
+ *
4
+ * Canonical location (Phase 6b of the CLI Runtime Convergence roadmap,
5
+ * 2026-04-09). Previously lived at `../../lib/ws-agent-handler.js`; that
6
+ * path is retained as an `@deprecated` re-export shim for backwards
7
+ * compatibility.
8
+ *
9
+ * Handles agent session messages over WebSocket. Consumes agent-core's
10
+ * agentLoop generator and routes events to the client via the interaction
11
+ * adapter.
12
+ */
13
+
14
+ import { agentLoop, formatToolArgs } from "../../runtime/agent-core.js";
15
+ import {
16
+ detectTaskType,
17
+ selectModelForTask,
18
+ } from "../../lib/task-model-selector.js";
19
+ import { PlanState } from "../../lib/plan-mode.js";
20
+ import { CLISlotFiller } from "../../lib/slot-filler.js";
21
+ import { createAbortError, isAbortError } from "../../lib/abort-utils.js";
22
+
23
+ export class WSAgentHandler {
24
+ /**
25
+ * @param {object} options
26
+ * @param {import("./ws-session-gateway.js").Session} options.session
27
+ * @param {import("../../lib/interaction-adapter.js").WebSocketInteractionAdapter} options.interaction
28
+ * @param {object} [options.db]
29
+ */
30
+ constructor({ session, interaction, db }) {
31
+ this.session = session;
32
+ this.interaction = interaction;
33
+ this.db = db || null;
34
+ this._processing = false;
35
+ this._abortController = null;
36
+ this._activeRequestId = null;
37
+ }
38
+
39
+ /**
40
+ * Handle a user message — one turn of the agentic loop.
41
+ *
42
+ * @param {string} userMessage
43
+ * @param {string} [requestId] - id from ws message for response correlation
44
+ */
45
+ async handleMessage(userMessage, requestId) {
46
+ if (this._processing) {
47
+ this.interaction.emit("error", {
48
+ requestId,
49
+ code: "BUSY",
50
+ message: "Session is currently processing a message",
51
+ });
52
+ return;
53
+ }
54
+
55
+ this._processing = true;
56
+ const abortController = new AbortController();
57
+ this._abortController = abortController;
58
+ this._activeRequestId = requestId || null;
59
+
60
+ try {
61
+ const { session } = this;
62
+
63
+ // Add user message
64
+ session.messages.push({ role: "user", content: userMessage });
65
+
66
+ // Auto-select model based on task type
67
+ let activeModel = session.model;
68
+ const taskDetection = detectTaskType(userMessage);
69
+ if (taskDetection.confidence > 0.3) {
70
+ const recommended = selectModelForTask(
71
+ session.provider,
72
+ taskDetection.taskType,
73
+ );
74
+ if (recommended && recommended !== activeModel) {
75
+ activeModel = recommended;
76
+ this.interaction.emit("model-switch", {
77
+ requestId,
78
+ from: session.model,
79
+ to: activeModel,
80
+ reason: taskDetection.name,
81
+ });
82
+ }
83
+ }
84
+
85
+ // Create slot filler for interactive parameter collection
86
+ const slotFiller = new CLISlotFiller({
87
+ interaction: this.interaction,
88
+ db: this.db,
89
+ });
90
+
91
+ // Run agent loop
92
+ const loopOptions = {
93
+ provider: session.provider,
94
+ model: activeModel,
95
+ baseUrl: session.baseUrl || "http://localhost:11434",
96
+ apiKey: session.apiKey,
97
+ contextEngine: session.contextEngine,
98
+ hookDb: this.db,
99
+ cwd: session.projectRoot,
100
+ sessionId: session.id,
101
+ planManager: session.planManager,
102
+ enabledToolNames: session.enabledToolNames || null,
103
+ hostManagedToolPolicy: session.hostManagedToolPolicy || null,
104
+ extraToolDefinitions: session.externalToolDefinitions || [],
105
+ externalToolDescriptors: session.externalToolDescriptors || {},
106
+ externalToolExecutors: session.externalToolExecutors || {},
107
+ mcpClient: session.mcpClient || null,
108
+ slotFiller,
109
+ interaction: this.interaction,
110
+ signal: abortController.signal,
111
+ };
112
+
113
+ for await (const event of agentLoop(session.messages, loopOptions)) {
114
+ switch (event.type) {
115
+ case "slot-filling":
116
+ this.interaction.emit("slot-filling", {
117
+ requestId,
118
+ slot: event.slot,
119
+ question: event.question,
120
+ });
121
+ break;
122
+
123
+ case "tool-executing":
124
+ this.interaction.emit("tool-executing", {
125
+ requestId,
126
+ tool: event.tool,
127
+ args: event.args,
128
+ display: formatToolArgs(event.tool, event.args),
129
+ });
130
+ break;
131
+
132
+ case "tool-result":
133
+ this.interaction.emit("tool-result", {
134
+ requestId,
135
+ tool: event.tool,
136
+ result: event.result,
137
+ error: event.error,
138
+ });
139
+ break;
140
+
141
+ case "response-complete":
142
+ if (event.content) {
143
+ session.messages.push({
144
+ role: "assistant",
145
+ content: event.content,
146
+ });
147
+ }
148
+ this.interaction.emit("response-complete", {
149
+ requestId,
150
+ content: event.content,
151
+ });
152
+ break;
153
+ }
154
+ }
155
+
156
+ // Update last activity
157
+ session.lastActivity = new Date().toISOString();
158
+ } catch (err) {
159
+ if (isAbortError(err) || abortController.signal.aborted) {
160
+ return;
161
+ }
162
+
163
+ this.interaction.emit("error", {
164
+ requestId,
165
+ code: "AGENT_ERROR",
166
+ message: err.message,
167
+ });
168
+
169
+ // Record error in context engine
170
+ if (this.session.contextEngine) {
171
+ this.session.contextEngine.recordError({
172
+ step: "ws-agent-loop",
173
+ message: err.message,
174
+ });
175
+ }
176
+ } finally {
177
+ this._processing = false;
178
+ if (this._abortController === abortController) {
179
+ this._abortController = null;
180
+ }
181
+ if (this._activeRequestId === requestId) {
182
+ this._activeRequestId = null;
183
+ }
184
+ }
185
+ }
186
+
187
+ async interrupt() {
188
+ const wasProcessing = this._processing;
189
+ const interruptedRequestId = this._activeRequestId || null;
190
+ const reason = createAbortError("Session interrupted by client");
191
+
192
+ if (this._abortController && !this._abortController.signal.aborted) {
193
+ this._abortController.abort(reason);
194
+ }
195
+
196
+ if (typeof this.interaction?.rejectAllPending === "function") {
197
+ this.interaction.rejectAllPending(reason);
198
+ }
199
+
200
+ return {
201
+ sessionId: this.session?.id || null,
202
+ interrupted: true,
203
+ wasProcessing,
204
+ interruptedRequestId,
205
+ };
206
+ }
207
+
208
+ destroy() {
209
+ const reason = createAbortError("Session closed");
210
+ if (this._abortController && !this._abortController.signal.aborted) {
211
+ this._abortController.abort(reason);
212
+ }
213
+ if (typeof this.interaction?.rejectAllPending === "function") {
214
+ this.interaction.rejectAllPending(reason);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Handle slash commands within the session.
220
+ *
221
+ * @param {string} command - e.g. "/plan enter", "/model qwen2:7b"
222
+ * @param {string} [requestId]
223
+ */
224
+ async handleSlashCommand(command, requestId) {
225
+ const [cmd, ...args] = command.trim().split(/\s+/);
226
+ const arg = args.join(" ").trim();
227
+ const { session } = this;
228
+
229
+ switch (cmd) {
230
+ case "/model":
231
+ if (arg) {
232
+ session.model = arg;
233
+ this.interaction.emit("command-response", {
234
+ requestId,
235
+ command: cmd,
236
+ result: { model: arg },
237
+ });
238
+ } else {
239
+ this.interaction.emit("command-response", {
240
+ requestId,
241
+ command: cmd,
242
+ result: { model: session.model },
243
+ });
244
+ }
245
+ break;
246
+
247
+ case "/provider": {
248
+ const supported = [
249
+ "ollama",
250
+ "anthropic",
251
+ "openai",
252
+ "deepseek",
253
+ "dashscope",
254
+ "mistral",
255
+ "gemini",
256
+ "volcengine",
257
+ ];
258
+ if (arg && supported.includes(arg)) {
259
+ session.provider = arg;
260
+ this.interaction.emit("command-response", {
261
+ requestId,
262
+ command: cmd,
263
+ result: { provider: arg },
264
+ });
265
+ } else {
266
+ this.interaction.emit("command-response", {
267
+ requestId,
268
+ command: cmd,
269
+ result: { provider: session.provider, available: supported },
270
+ });
271
+ }
272
+ break;
273
+ }
274
+
275
+ case "/clear":
276
+ session.messages.length = 1; // Keep system prompt
277
+ this.interaction.emit("command-response", {
278
+ requestId,
279
+ command: cmd,
280
+ result: { cleared: true },
281
+ });
282
+ break;
283
+
284
+ case "/compact":
285
+ if (session.contextEngine && session.messages.length > 5) {
286
+ const compacted = session.contextEngine.smartCompact(
287
+ session.messages,
288
+ );
289
+ session.messages.length = 0;
290
+ session.messages.push(...compacted);
291
+ } else if (session.messages.length > 5) {
292
+ const systemMsg = session.messages[0];
293
+ const recent = session.messages.slice(-4);
294
+ session.messages.length = 0;
295
+ session.messages.push(systemMsg, ...recent);
296
+ }
297
+ this.interaction.emit("command-response", {
298
+ requestId,
299
+ command: cmd,
300
+ result: { messageCount: session.messages.length },
301
+ });
302
+ break;
303
+
304
+ case "/task":
305
+ if (arg === "clear") {
306
+ if (session.contextEngine) session.contextEngine.clearTask();
307
+ this.interaction.emit("command-response", {
308
+ requestId,
309
+ command: cmd,
310
+ result: { cleared: true },
311
+ });
312
+ } else if (arg) {
313
+ if (session.contextEngine) session.contextEngine.setTask(arg);
314
+ this.interaction.emit("command-response", {
315
+ requestId,
316
+ command: cmd,
317
+ result: { task: arg },
318
+ });
319
+ } else {
320
+ this.interaction.emit("command-response", {
321
+ requestId,
322
+ command: cmd,
323
+ result: {
324
+ task: session.contextEngine?.taskContext?.objective || null,
325
+ },
326
+ });
327
+ }
328
+ break;
329
+
330
+ case "/stats":
331
+ if (session.contextEngine) {
332
+ const stats = session.contextEngine.getStats();
333
+ this.interaction.emit("command-response", {
334
+ requestId,
335
+ command: cmd,
336
+ result: stats,
337
+ });
338
+ } else {
339
+ this.interaction.emit("command-response", {
340
+ requestId,
341
+ command: cmd,
342
+ result: { error: "Context engine not available" },
343
+ });
344
+ }
345
+ break;
346
+
347
+ case "/session":
348
+ this.interaction.emit("command-response", {
349
+ requestId,
350
+ command: cmd,
351
+ result: {
352
+ id: session.id,
353
+ type: session.type,
354
+ provider: session.provider,
355
+ model: session.model,
356
+ messageCount: session.messages.length,
357
+ projectRoot: session.projectRoot,
358
+ createdAt: session.createdAt,
359
+ lastActivity: session.lastActivity,
360
+ },
361
+ });
362
+ break;
363
+
364
+ case "/plan":
365
+ this._handlePlanCommand(arg, requestId);
366
+ break;
367
+
368
+ default:
369
+ this.interaction.emit("command-response", {
370
+ requestId,
371
+ command: cmd,
372
+ result: { error: `Unknown command: ${cmd}` },
373
+ });
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Handle /plan sub-commands.
379
+ */
380
+ _handlePlanCommand(subCmd, requestId) {
381
+ const planManager = this.session.planManager;
382
+
383
+ if (!subCmd || subCmd === "enter") {
384
+ if (planManager.isActive()) {
385
+ this.interaction.emit("command-response", {
386
+ requestId,
387
+ command: "/plan",
388
+ result: { error: "Already in plan mode" },
389
+ });
390
+ } else {
391
+ planManager.enterPlanMode({ title: "Agent Plan" });
392
+ this.session.messages.push({
393
+ role: "system",
394
+ content:
395
+ "[PLAN MODE ACTIVE] You are now in plan mode. You can read files, search, and analyze — but write/execute tools are blocked. Any blocked tool calls will be recorded as plan items. Analyze the task thoroughly, then the user will approve your plan.",
396
+ });
397
+ this.interaction.emit("command-response", {
398
+ requestId,
399
+ command: "/plan",
400
+ result: { state: "analyzing", message: "Entered plan mode" },
401
+ });
402
+ }
403
+ } else if (subCmd === "show") {
404
+ if (!planManager.isActive()) {
405
+ this.interaction.emit("command-response", {
406
+ requestId,
407
+ command: "/plan show",
408
+ result: { error: "Not in plan mode" },
409
+ });
410
+ } else {
411
+ this.interaction.emit("plan-ready", {
412
+ requestId,
413
+ summary: planManager.generatePlanSummary(),
414
+ risk: planManager.getRiskAssessment(),
415
+ items: planManager.currentPlan?.items || [],
416
+ });
417
+ }
418
+ } else if (subCmd === "approve" || subCmd === "yes") {
419
+ if (!planManager.isActive()) {
420
+ this.interaction.emit("command-response", {
421
+ requestId,
422
+ command: "/plan approve",
423
+ result: { error: "No plan to approve" },
424
+ });
425
+ } else if (
426
+ !planManager.currentPlan ||
427
+ planManager.currentPlan.items.length === 0
428
+ ) {
429
+ this.interaction.emit("command-response", {
430
+ requestId,
431
+ command: "/plan approve",
432
+ result: { error: "Plan has no items" },
433
+ });
434
+ } else {
435
+ planManager.approvePlan();
436
+ this.session.messages.push({
437
+ role: "system",
438
+ content: `[PLAN APPROVED] The user has approved your plan with ${planManager.currentPlan.items.length} items. You can now use all tools including write_file, edit_file, run_shell, git, and run_skill. Execute the plan items in order.`,
439
+ });
440
+ this.interaction.emit("command-response", {
441
+ requestId,
442
+ command: "/plan approve",
443
+ result: {
444
+ state: PlanState.APPROVED,
445
+ itemCount: planManager.currentPlan.items.length,
446
+ },
447
+ });
448
+ }
449
+ } else if (subCmd === "reject" || subCmd === "no") {
450
+ if (planManager.isActive()) {
451
+ planManager.rejectPlan("User rejected");
452
+ this.interaction.emit("command-response", {
453
+ requestId,
454
+ command: "/plan reject",
455
+ result: { state: PlanState.REJECTED },
456
+ });
457
+ } else {
458
+ this.interaction.emit("command-response", {
459
+ requestId,
460
+ command: "/plan reject",
461
+ result: { error: "No plan to reject" },
462
+ });
463
+ }
464
+ } else if (subCmd === "exit") {
465
+ if (planManager.isActive()) {
466
+ planManager.exitPlanMode({ savePlan: true });
467
+ }
468
+ this.interaction.emit("command-response", {
469
+ requestId,
470
+ command: "/plan exit",
471
+ result: { state: PlanState.INACTIVE },
472
+ });
473
+ } else {
474
+ this.interaction.emit("command-response", {
475
+ requestId,
476
+ command: "/plan",
477
+ result: {
478
+ error: `Unknown /plan subcommand: ${subCmd}`,
479
+ available: ["enter", "show", "approve", "reject", "exit"],
480
+ },
481
+ });
482
+ }
483
+ }
484
+ }