@nodetool-ai/cli-chat 0.0.0-stage → 0.8.9-beta.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 +13 -2
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/ExecutionTree.d.ts +20 -0
- package/dist/ExecutionTree.d.ts.map +1 -0
- package/dist/ExecutionTree.js +100 -0
- package/dist/ExecutionTree.js.map +1 -0
- package/dist/agent-toolbelt.d.ts +15 -0
- package/dist/agent-toolbelt.d.ts.map +1 -0
- package/dist/agent-toolbelt.js +28 -0
- package/dist/agent-toolbelt.js.map +1 -0
- package/dist/app.d.ts +40 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +1070 -0
- package/dist/app.js.map +1 -0
- package/dist/bash-tool.d.ts +13 -0
- package/dist/bash-tool.d.ts.map +1 -0
- package/dist/bash-tool.js +85 -0
- package/dist/bash-tool.js.map +1 -0
- package/dist/chat-codeact.d.ts +93 -0
- package/dist/chat-codeact.d.ts.map +1 -0
- package/dist/chat-codeact.js +179 -0
- package/dist/chat-codeact.js.map +1 -0
- package/dist/chat-context.d.ts +18 -0
- package/dist/chat-context.d.ts.map +1 -0
- package/dist/chat-context.js +32 -0
- package/dist/chat-context.js.map +1 -0
- package/dist/chat-entry.d.ts +13 -0
- package/dist/chat-entry.d.ts.map +1 -0
- package/dist/chat-entry.js +233 -0
- package/dist/chat-entry.js.map +1 -0
- package/dist/chat-media.d.ts +3 -0
- package/dist/chat-media.d.ts.map +1 -0
- package/dist/chat-media.js +46 -0
- package/dist/chat-media.js.map +1 -0
- package/dist/chat-prompts.d.ts +20 -0
- package/dist/chat-prompts.d.ts.map +1 -0
- package/dist/chat-prompts.js +42 -0
- package/dist/chat-prompts.js.map +1 -0
- package/dist/chat-sessions.d.ts +25 -0
- package/dist/chat-sessions.d.ts.map +1 -0
- package/dist/chat-sessions.js +153 -0
- package/dist/chat-sessions.js.map +1 -0
- package/dist/commands/agent.d.ts +33 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +516 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/tools.d.ts +11 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +206 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/diagnose.d.ts +139 -0
- package/dist/diagnose.d.ts.map +1 -0
- package/dist/diagnose.js +435 -0
- package/dist/diagnose.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +9 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +53 -0
- package/dist/markdown.js.map +1 -0
- package/dist/nodetool.d.ts +3 -0
- package/dist/nodetool.d.ts.map +1 -0
- package/dist/nodetool.js +4 -0
- package/dist/nodetool.js.map +1 -0
- package/dist/permission-gate.d.ts +55 -0
- package/dist/permission-gate.d.ts.map +1 -0
- package/dist/permission-gate.js +118 -0
- package/dist/permission-gate.js.map +1 -0
- package/dist/readline-input.d.ts +23 -0
- package/dist/readline-input.d.ts.map +1 -0
- package/dist/readline-input.js +210 -0
- package/dist/readline-input.js.map +1 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +146 -0
- package/dist/settings.js.map +1 -0
- package/dist/stdin.d.ts +52 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +363 -0
- package/dist/stdin.js.map +1 -0
- package/dist/terminal-clipboard.d.ts +3 -0
- package/dist/terminal-clipboard.d.ts.map +1 -0
- package/dist/terminal-clipboard.js +24 -0
- package/dist/terminal-clipboard.js.map +1 -0
- package/dist/terminal-mouse.d.ts +24 -0
- package/dist/terminal-mouse.d.ts.map +1 -0
- package/dist/terminal-mouse.js +84 -0
- package/dist/terminal-mouse.js.map +1 -0
- package/dist/terminal-screen.d.ts +41 -0
- package/dist/terminal-screen.d.ts.map +1 -0
- package/dist/terminal-screen.js +228 -0
- package/dist/terminal-screen.js.map +1 -0
- package/dist/tool-format.d.ts +54 -0
- package/dist/tool-format.d.ts.map +1 -0
- package/dist/tool-format.js +360 -0
- package/dist/tool-format.js.map +1 -0
- package/dist/useExecutionState.d.ts +44 -0
- package/dist/useExecutionState.d.ts.map +1 -0
- package/dist/useExecutionState.js +285 -0
- package/dist/useExecutionState.js.map +1 -0
- package/dist/websocket-client.d.ts +159 -0
- package/dist/websocket-client.d.ts.map +1 -0
- package/dist/websocket-client.js +677 -0
- package/dist/websocket-client.js.map +1 -0
- package/package.json +73 -4
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for connecting the CLI to a NodeTool server.
|
|
3
|
+
* Handles chat (stateful, with thread history), inference (stateless, for agent mode),
|
|
4
|
+
* and workflow commands (run_job, cancel_job, get_status).
|
|
5
|
+
*/
|
|
6
|
+
import WebSocket from "ws";
|
|
7
|
+
import { processingMessageSchema } from "@nodetool-ai/protocol";
|
|
8
|
+
import { isNumber, isRecord, isString } from "@nodetool-ai/cli-common/predicates";
|
|
9
|
+
/** Read `key` as a string, or report it absent when the wire says otherwise. */
|
|
10
|
+
function wireString(frame, key) {
|
|
11
|
+
const value = frame[key];
|
|
12
|
+
return isString(value) ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
/** Read `key` as a number, or report it absent when the wire says otherwise. */
|
|
15
|
+
function wireNumber(frame, key) {
|
|
16
|
+
const value = frame[key];
|
|
17
|
+
return isNumber(value) ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
function toolCallArgs(frame) {
|
|
20
|
+
const args = frame["args"];
|
|
21
|
+
return isRecord(args) ? args : {};
|
|
22
|
+
}
|
|
23
|
+
/** Read a `message` frame's `tool_calls`; anything else reads as none. */
|
|
24
|
+
function parseToolCalls(frame) {
|
|
25
|
+
const raw = frame["tool_calls"];
|
|
26
|
+
if (!Array.isArray(raw))
|
|
27
|
+
return [];
|
|
28
|
+
const calls = [];
|
|
29
|
+
for (const entry of raw) {
|
|
30
|
+
if (!isRecord(entry))
|
|
31
|
+
continue;
|
|
32
|
+
const call = entry;
|
|
33
|
+
calls.push({
|
|
34
|
+
id: wireString(call, "id") ?? "",
|
|
35
|
+
name: wireString(call, "name") ?? "",
|
|
36
|
+
args: toolCallArgs(call)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return calls;
|
|
40
|
+
}
|
|
41
|
+
function parsePlan(value) {
|
|
42
|
+
if (!isRecord(value) ||
|
|
43
|
+
!isString(value["title"]) ||
|
|
44
|
+
!Array.isArray(value["tasks"]))
|
|
45
|
+
return null;
|
|
46
|
+
const tasks = [];
|
|
47
|
+
for (const task of value["tasks"]) {
|
|
48
|
+
if (!isRecord(task) ||
|
|
49
|
+
!isString(task["id"]) ||
|
|
50
|
+
!isString(task["title"]) ||
|
|
51
|
+
!Array.isArray(task["steps"]))
|
|
52
|
+
return null;
|
|
53
|
+
const steps = [];
|
|
54
|
+
for (const step of task["steps"]) {
|
|
55
|
+
if (!isRecord(step) ||
|
|
56
|
+
!isString(step["id"]) ||
|
|
57
|
+
!isString(step["instructions"]))
|
|
58
|
+
return null;
|
|
59
|
+
steps.push({ id: step["id"], instructions: step["instructions"] });
|
|
60
|
+
}
|
|
61
|
+
const dependencies = task["depends_on"];
|
|
62
|
+
tasks.push({
|
|
63
|
+
id: task["id"],
|
|
64
|
+
title: task["title"],
|
|
65
|
+
depends_on: Array.isArray(dependencies)
|
|
66
|
+
? dependencies.filter(isString)
|
|
67
|
+
: [],
|
|
68
|
+
steps
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { title: value["title"], tasks };
|
|
72
|
+
}
|
|
73
|
+
function assistantText(content) {
|
|
74
|
+
if (isString(content))
|
|
75
|
+
return content;
|
|
76
|
+
if (!Array.isArray(content))
|
|
77
|
+
return "";
|
|
78
|
+
return content
|
|
79
|
+
.flatMap((block) => isRecord(block) && block["type"] === "text" && isString(block["text"])
|
|
80
|
+
? [block["text"]]
|
|
81
|
+
: [])
|
|
82
|
+
.join("");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Turn one decoded frame into the domain value it stands for, or null when the
|
|
86
|
+
* client has nothing to do with it. Fields the wire gets wrong read as absent
|
|
87
|
+
* and fall back to the same defaults the generators used to apply inline.
|
|
88
|
+
*/
|
|
89
|
+
function parseServerFrame(frame) {
|
|
90
|
+
const type = wireString(frame, "type");
|
|
91
|
+
// A command-level failure has no `type`, only `error`.
|
|
92
|
+
if (type === undefined) {
|
|
93
|
+
const error = wireString(frame, "error");
|
|
94
|
+
return error === undefined ? null : { type: "error", message: error };
|
|
95
|
+
}
|
|
96
|
+
switch (type) {
|
|
97
|
+
case "chunk":
|
|
98
|
+
return {
|
|
99
|
+
type: "chunk",
|
|
100
|
+
content: wireString(frame, "content") ?? "",
|
|
101
|
+
done: frame["done"] === true
|
|
102
|
+
};
|
|
103
|
+
case "message": {
|
|
104
|
+
const role = wireString(frame, "role");
|
|
105
|
+
if (role === "tool") {
|
|
106
|
+
return {
|
|
107
|
+
type: "tool_result",
|
|
108
|
+
toolCallId: wireString(frame, "tool_call_id") ?? "",
|
|
109
|
+
name: wireString(frame, "name") ?? "",
|
|
110
|
+
content: wireString(frame, "content") ?? ""
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (role === "assistant") {
|
|
114
|
+
return {
|
|
115
|
+
type: "assistant_message",
|
|
116
|
+
toolCalls: parseToolCalls(frame),
|
|
117
|
+
content: frame["content"]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
case "tool_call":
|
|
123
|
+
return {
|
|
124
|
+
type: "tool_call",
|
|
125
|
+
id: wireString(frame, "tool_call_id") ?? wireString(frame, "id") ?? "",
|
|
126
|
+
name: wireString(frame, "name") ?? "",
|
|
127
|
+
args: toolCallArgs(frame)
|
|
128
|
+
};
|
|
129
|
+
case "tool_approval_request":
|
|
130
|
+
return {
|
|
131
|
+
type,
|
|
132
|
+
approvalId: wireString(frame, "approval_id") ?? "",
|
|
133
|
+
threadId: wireString(frame, "thread_id") ?? "",
|
|
134
|
+
toolName: wireString(frame, "tool_name") ?? "",
|
|
135
|
+
category: wireString(frame, "category") ?? "",
|
|
136
|
+
message: wireString(frame, "message") ?? "",
|
|
137
|
+
description: wireString(frame, "description") ?? "",
|
|
138
|
+
args: toolCallArgs(frame)
|
|
139
|
+
};
|
|
140
|
+
case "plan_approval_request": {
|
|
141
|
+
const plan = parsePlan(frame["plan"]);
|
|
142
|
+
if (!plan)
|
|
143
|
+
return {
|
|
144
|
+
type: "error",
|
|
145
|
+
message: "The server sent an invalid approval plan."
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
type,
|
|
149
|
+
approvalId: wireString(frame, "approval_id") ?? "",
|
|
150
|
+
threadId: wireString(frame, "thread_id") ?? null,
|
|
151
|
+
plan
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "secret_request":
|
|
155
|
+
return {
|
|
156
|
+
type,
|
|
157
|
+
approvalId: wireString(frame, "approval_id") ?? "",
|
|
158
|
+
threadId: wireString(frame, "thread_id") ?? "",
|
|
159
|
+
key: wireString(frame, "key") ?? "",
|
|
160
|
+
description: wireString(frame, "description") ?? null,
|
|
161
|
+
reason: wireString(frame, "reason") ?? null,
|
|
162
|
+
helpUrl: wireString(frame, "help_url") ?? null
|
|
163
|
+
};
|
|
164
|
+
case "job_update":
|
|
165
|
+
return {
|
|
166
|
+
type: "job_update",
|
|
167
|
+
status: wireString(frame, "status") ?? "unknown",
|
|
168
|
+
jobId: wireString(frame, "job_id"),
|
|
169
|
+
workflowId: wireString(frame, "workflow_id"),
|
|
170
|
+
error: wireString(frame, "error"),
|
|
171
|
+
result: frame["result"]
|
|
172
|
+
};
|
|
173
|
+
case "node_update":
|
|
174
|
+
return {
|
|
175
|
+
type: "node_update",
|
|
176
|
+
nodeId: wireString(frame, "node_id") ?? "",
|
|
177
|
+
status: wireString(frame, "status") ?? "unknown",
|
|
178
|
+
error: wireString(frame, "error")
|
|
179
|
+
};
|
|
180
|
+
case "output_update":
|
|
181
|
+
return {
|
|
182
|
+
type: "output_update",
|
|
183
|
+
nodeId: wireString(frame, "node_id") ?? "",
|
|
184
|
+
value: frame["value"],
|
|
185
|
+
outputType: wireString(frame, "output_type")
|
|
186
|
+
};
|
|
187
|
+
case "node_progress":
|
|
188
|
+
return {
|
|
189
|
+
type: "node_progress",
|
|
190
|
+
nodeId: wireString(frame, "node_id") ?? "",
|
|
191
|
+
progress: wireNumber(frame, "progress") ?? 0,
|
|
192
|
+
total: wireNumber(frame, "total")
|
|
193
|
+
};
|
|
194
|
+
case "error":
|
|
195
|
+
return {
|
|
196
|
+
type: "error",
|
|
197
|
+
message: wireString(frame, "message") ?? "Unknown error"
|
|
198
|
+
};
|
|
199
|
+
case "inference_done":
|
|
200
|
+
return { type: "inference_done" };
|
|
201
|
+
case "generation_stopped":
|
|
202
|
+
return { type: "generation_stopped" };
|
|
203
|
+
default: {
|
|
204
|
+
const parsed = processingMessageSchema.safeParse(frame);
|
|
205
|
+
return parsed.success
|
|
206
|
+
? { type: "processing", message: parsed.data }
|
|
207
|
+
: null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export class WebSocketChatClient {
|
|
212
|
+
wsUrl;
|
|
213
|
+
ws = null;
|
|
214
|
+
disconnectedMessage = "Not connected to the NodeTool server.";
|
|
215
|
+
contentQueue = [];
|
|
216
|
+
contentWaiters = [];
|
|
217
|
+
constructor(wsUrl) {
|
|
218
|
+
this.wsUrl = wsUrl;
|
|
219
|
+
}
|
|
220
|
+
async connect() {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const ws = new WebSocket(this.wsUrl);
|
|
223
|
+
this.ws = ws;
|
|
224
|
+
this.disconnectedMessage =
|
|
225
|
+
"Connection to the NodeTool server closed before the response completed.";
|
|
226
|
+
// Guard so the connect promise settles exactly once: the "error" listener
|
|
227
|
+
// stays attached for the socket's lifetime, so without this a post-connect
|
|
228
|
+
// error would call reject() on an already-resolved promise and be silently
|
|
229
|
+
// swallowed (and an unhandled "error" event would otherwise crash Node).
|
|
230
|
+
let settled = false;
|
|
231
|
+
ws.on("open", () => {
|
|
232
|
+
if (this.ws !== ws) {
|
|
233
|
+
ws.close();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Switch server to text/JSON mode
|
|
237
|
+
ws.send(JSON.stringify({ command: "set_mode", data: { mode: "text" } }));
|
|
238
|
+
if (!settled) {
|
|
239
|
+
settled = true;
|
|
240
|
+
resolve();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
ws.on("error", (err) => {
|
|
244
|
+
this.disconnectedMessage = `NodeTool connection failed: ${err.message}`;
|
|
245
|
+
if (this.ws === ws)
|
|
246
|
+
this.ws = null;
|
|
247
|
+
if (!settled) {
|
|
248
|
+
settled = true;
|
|
249
|
+
reject(err);
|
|
250
|
+
this.drainWaiters();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// Error after a successful connect: tear down and unblock any waiting
|
|
254
|
+
// generators so they terminate instead of hanging forever.
|
|
255
|
+
this.ws = null;
|
|
256
|
+
this.drainWaiters();
|
|
257
|
+
});
|
|
258
|
+
ws.on("message", (data) => {
|
|
259
|
+
try {
|
|
260
|
+
const msg = JSON.parse(isString(data) ? data : data.toString("utf8"));
|
|
261
|
+
if (isRecord(msg))
|
|
262
|
+
this.handleMessage(msg);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// ignore malformed messages
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
ws.on("close", () => {
|
|
269
|
+
if (this.ws === ws)
|
|
270
|
+
this.ws = null;
|
|
271
|
+
if (!settled) {
|
|
272
|
+
settled = true;
|
|
273
|
+
reject(new Error(this.disconnectedMessage));
|
|
274
|
+
}
|
|
275
|
+
this.drainWaiters();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/** A dropped connection is a failed response, never a successful completion. */
|
|
280
|
+
drainWaiters() {
|
|
281
|
+
const waiters = this.contentWaiters;
|
|
282
|
+
this.contentWaiters = [];
|
|
283
|
+
for (const waiter of waiters)
|
|
284
|
+
waiter({ type: "error", message: this.disconnectedMessage });
|
|
285
|
+
}
|
|
286
|
+
handleMessage(msg) {
|
|
287
|
+
// Auto-respond to server pings
|
|
288
|
+
if (msg["type"] === "ping") {
|
|
289
|
+
this.send({ type: "pong", ts: Date.now() / 1000 });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// Route content events to waiting generators
|
|
293
|
+
const parsed = parseServerFrame(msg);
|
|
294
|
+
if (!parsed)
|
|
295
|
+
return;
|
|
296
|
+
const processing = processingMessageSchema.safeParse(msg);
|
|
297
|
+
const frame = {
|
|
298
|
+
...parsed,
|
|
299
|
+
scopeThreadId: wireString(msg, "thread_id"),
|
|
300
|
+
parentToolCallId: wireString(msg, "parent_tool_call_id"),
|
|
301
|
+
...(processing.success && { processing: processing.data })
|
|
302
|
+
};
|
|
303
|
+
const waiter = this.contentWaiters.shift();
|
|
304
|
+
if (waiter) {
|
|
305
|
+
waiter(frame);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
this.contentQueue.push(frame);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
send(data) {
|
|
312
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
313
|
+
throw new Error(this.disconnectedMessage);
|
|
314
|
+
}
|
|
315
|
+
this.ws.send(JSON.stringify(data));
|
|
316
|
+
}
|
|
317
|
+
nextContent(signal) {
|
|
318
|
+
if (signal?.aborted)
|
|
319
|
+
return Promise.resolve(null);
|
|
320
|
+
const queued = this.contentQueue.shift();
|
|
321
|
+
if (queued)
|
|
322
|
+
return Promise.resolve(queued);
|
|
323
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
324
|
+
return Promise.resolve({
|
|
325
|
+
type: "error",
|
|
326
|
+
message: this.disconnectedMessage
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return new Promise((resolve) => {
|
|
330
|
+
const receive = (event) => {
|
|
331
|
+
signal?.removeEventListener("abort", cancel);
|
|
332
|
+
resolve(event);
|
|
333
|
+
};
|
|
334
|
+
const cancel = () => {
|
|
335
|
+
const index = this.contentWaiters.indexOf(receive);
|
|
336
|
+
if (index !== -1)
|
|
337
|
+
this.contentWaiters.splice(index, 1);
|
|
338
|
+
receive(null);
|
|
339
|
+
};
|
|
340
|
+
this.contentWaiters.push(receive);
|
|
341
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/** Stateful chat — messages are saved to DB, thread history is loaded. */
|
|
345
|
+
async *chat(content, threadId, model, provider, tools, options = {}) {
|
|
346
|
+
if (options.signal?.aborted) {
|
|
347
|
+
yield { type: "done" };
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
this.contentQueue.length = 0;
|
|
351
|
+
this.send({
|
|
352
|
+
command: "chat_message",
|
|
353
|
+
data: {
|
|
354
|
+
role: "user",
|
|
355
|
+
content,
|
|
356
|
+
thread_id: threadId,
|
|
357
|
+
model,
|
|
358
|
+
provider,
|
|
359
|
+
tools: tools ?? [],
|
|
360
|
+
...(options.permissionMode && {
|
|
361
|
+
permission_mode: options.permissionMode
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
const cancel = () => {
|
|
366
|
+
if (this.ws?.readyState === WebSocket.OPEN)
|
|
367
|
+
this.stop(threadId);
|
|
368
|
+
};
|
|
369
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
370
|
+
let streamedText = "";
|
|
371
|
+
const announcedCalls = new Set();
|
|
372
|
+
const completedCalls = new Set();
|
|
373
|
+
try {
|
|
374
|
+
while (true) {
|
|
375
|
+
const event = await this.nextContent(options.signal);
|
|
376
|
+
if (!event) {
|
|
377
|
+
yield { type: "done" };
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (event.scopeThreadId && event.scopeThreadId !== threadId)
|
|
381
|
+
continue;
|
|
382
|
+
if (event.type === "chunk") {
|
|
383
|
+
if (event.processing?.type === "chunk" &&
|
|
384
|
+
(event.processing.thinking ||
|
|
385
|
+
event.parentToolCallId ||
|
|
386
|
+
event.processing.subtask_depth)) {
|
|
387
|
+
yield { type: "processing", message: event.processing };
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (event.content) {
|
|
391
|
+
if (!event.parentToolCallId)
|
|
392
|
+
streamedText += event.content;
|
|
393
|
+
yield { type: "chunk", content: event.content };
|
|
394
|
+
}
|
|
395
|
+
// A delegated loop finishing does not finish its parent turn.
|
|
396
|
+
if (event.done && !event.parentToolCallId) {
|
|
397
|
+
yield { type: "done" };
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (event.type === "assistant_message") {
|
|
402
|
+
if (event.content != null) {
|
|
403
|
+
const fullText = assistantText(event.content);
|
|
404
|
+
const text = fullText.startsWith(streamedText)
|
|
405
|
+
? fullText.slice(streamedText.length)
|
|
406
|
+
: fullText;
|
|
407
|
+
if (!event.parentToolCallId)
|
|
408
|
+
streamedText = "";
|
|
409
|
+
if (text || Array.isArray(event.content)) {
|
|
410
|
+
yield { type: "assistant_message", content: event.content, text };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const call of event.toolCalls) {
|
|
414
|
+
if (announcedCalls.has(call.id))
|
|
415
|
+
continue;
|
|
416
|
+
announcedCalls.add(call.id);
|
|
417
|
+
yield { type: "tool_call", ...call };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else if (event.type === "tool_call") {
|
|
421
|
+
yield {
|
|
422
|
+
type: "client_tool_call",
|
|
423
|
+
id: event.id,
|
|
424
|
+
name: event.name,
|
|
425
|
+
args: event.args,
|
|
426
|
+
threadId: event.scopeThreadId ?? threadId
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
else if (event.type === "tool_result") {
|
|
430
|
+
if (completedCalls.has(event.toolCallId))
|
|
431
|
+
continue;
|
|
432
|
+
completedCalls.add(event.toolCallId);
|
|
433
|
+
yield {
|
|
434
|
+
type: "tool_result",
|
|
435
|
+
id: event.toolCallId,
|
|
436
|
+
name: event.name,
|
|
437
|
+
content: event.content
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
else if (event.type === "output_update") {
|
|
441
|
+
yield {
|
|
442
|
+
type: "output_update",
|
|
443
|
+
node_id: event.nodeId,
|
|
444
|
+
value: event.value,
|
|
445
|
+
output_type: event.outputType
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
else if (event.type === "generation_stopped") {
|
|
449
|
+
yield { type: "done" };
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
else if (event.type === "error") {
|
|
453
|
+
yield { type: "error", message: event.message };
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
else if (event.type === "tool_approval_request" ||
|
|
457
|
+
event.type === "plan_approval_request" ||
|
|
458
|
+
event.type === "secret_request") {
|
|
459
|
+
yield event;
|
|
460
|
+
}
|
|
461
|
+
else if (event.type === "processing") {
|
|
462
|
+
const message = event.message;
|
|
463
|
+
if (message.type === "tool_call_update" &&
|
|
464
|
+
message.tool_call_id &&
|
|
465
|
+
!announcedCalls.has(message.tool_call_id)) {
|
|
466
|
+
announcedCalls.add(message.tool_call_id);
|
|
467
|
+
yield {
|
|
468
|
+
type: "tool_call",
|
|
469
|
+
id: message.tool_call_id,
|
|
470
|
+
name: message.name,
|
|
471
|
+
args: message.args
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
else if (message.type === "tool_result_update" &&
|
|
475
|
+
message.tool_call_id &&
|
|
476
|
+
!completedCalls.has(message.tool_call_id)) {
|
|
477
|
+
completedCalls.add(message.tool_call_id);
|
|
478
|
+
yield {
|
|
479
|
+
type: "tool_result",
|
|
480
|
+
id: message.tool_call_id,
|
|
481
|
+
name: message.name ?? "",
|
|
482
|
+
content: JSON.stringify(message.result)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
yield { type: "processing", message };
|
|
486
|
+
}
|
|
487
|
+
else if (event.processing) {
|
|
488
|
+
yield { type: "processing", message: event.processing };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** Stateless inference — takes full messages array, streams back, no DB. Used by agent mode. */
|
|
497
|
+
async *inference(messages, model, provider, tools) {
|
|
498
|
+
this.send({
|
|
499
|
+
command: "inference",
|
|
500
|
+
data: { messages, model, provider, tools: tools ?? [] }
|
|
501
|
+
});
|
|
502
|
+
while (true) {
|
|
503
|
+
const event = await this.nextContent();
|
|
504
|
+
if (!event) {
|
|
505
|
+
yield { type: "done" };
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (event.type === "chunk") {
|
|
509
|
+
if (event.content) {
|
|
510
|
+
yield { type: "chunk", content: event.content };
|
|
511
|
+
}
|
|
512
|
+
if (event.done) {
|
|
513
|
+
yield { type: "done" };
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else if (event.type === "tool_call") {
|
|
518
|
+
yield {
|
|
519
|
+
type: "tool_call",
|
|
520
|
+
id: event.id,
|
|
521
|
+
name: event.name,
|
|
522
|
+
args: event.args
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
else if (event.type === "inference_done" ||
|
|
526
|
+
event.type === "generation_stopped") {
|
|
527
|
+
yield { type: "done" };
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
else if (event.type === "error") {
|
|
531
|
+
yield { type: "error", message: event.message };
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Run a workflow job. Streams job_update, node_update, output_update events.
|
|
538
|
+
* Terminates when a terminal job_update is received (completed/failed/cancelled).
|
|
539
|
+
*/
|
|
540
|
+
async *runJob(opts) {
|
|
541
|
+
this.contentQueue.length = 0;
|
|
542
|
+
this.send({
|
|
543
|
+
command: "run_job",
|
|
544
|
+
data: {
|
|
545
|
+
workflow_id: opts.workflowId,
|
|
546
|
+
graph: opts.graph,
|
|
547
|
+
params: opts.params ?? {},
|
|
548
|
+
job_id: opts.jobId
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
yield* this.consumeJobEvents();
|
|
552
|
+
}
|
|
553
|
+
/** Reconnect to a running job's event stream. */
|
|
554
|
+
async *reconnectJob(jobId, workflowId) {
|
|
555
|
+
this.contentQueue.length = 0;
|
|
556
|
+
this.send({
|
|
557
|
+
command: "reconnect_job",
|
|
558
|
+
data: { job_id: jobId, workflow_id: workflowId }
|
|
559
|
+
});
|
|
560
|
+
yield* this.consumeJobEvents();
|
|
561
|
+
}
|
|
562
|
+
/** Shared job event consumer used by runJob and reconnectJob. */
|
|
563
|
+
async *consumeJobEvents() {
|
|
564
|
+
while (true) {
|
|
565
|
+
const event = await this.nextContent();
|
|
566
|
+
if (!event) {
|
|
567
|
+
yield { type: "done" };
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (event.type === "job_update") {
|
|
571
|
+
yield {
|
|
572
|
+
type: "job_update",
|
|
573
|
+
status: event.status,
|
|
574
|
+
job_id: event.jobId,
|
|
575
|
+
workflow_id: event.workflowId,
|
|
576
|
+
error: event.error,
|
|
577
|
+
result: event.result
|
|
578
|
+
};
|
|
579
|
+
if (["completed", "failed", "cancelled", "error"].includes(event.status)) {
|
|
580
|
+
yield { type: "done" };
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else if (event.type === "node_update") {
|
|
585
|
+
yield {
|
|
586
|
+
type: "node_update",
|
|
587
|
+
node_id: event.nodeId,
|
|
588
|
+
status: event.status,
|
|
589
|
+
error: event.error
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
else if (event.type === "output_update") {
|
|
593
|
+
yield {
|
|
594
|
+
type: "output_update",
|
|
595
|
+
node_id: event.nodeId,
|
|
596
|
+
value: event.value,
|
|
597
|
+
output_type: event.outputType
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
else if (event.type === "node_progress") {
|
|
601
|
+
yield {
|
|
602
|
+
type: "node_progress",
|
|
603
|
+
node_id: event.nodeId,
|
|
604
|
+
progress: event.progress,
|
|
605
|
+
total: event.total
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
else if (event.type === "error") {
|
|
609
|
+
yield { type: "error", message: event.message };
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
else if (event.type === "generation_stopped") {
|
|
613
|
+
yield { type: "done" };
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/** Cancel a running job. */
|
|
619
|
+
cancelJob(jobId) {
|
|
620
|
+
this.send({ command: "cancel_job", data: { job_id: jobId } });
|
|
621
|
+
}
|
|
622
|
+
/** Get status of a job or all active jobs. */
|
|
623
|
+
getStatus(jobId) {
|
|
624
|
+
this.send({ command: "get_status", data: jobId ? { job_id: jobId } : {} });
|
|
625
|
+
}
|
|
626
|
+
/** Stop in-progress generation. Pass threadId for chat, omit for inference. */
|
|
627
|
+
stop(threadId) {
|
|
628
|
+
const data = {};
|
|
629
|
+
if (threadId)
|
|
630
|
+
data.thread_id = threadId;
|
|
631
|
+
this.send({ command: "stop", data });
|
|
632
|
+
}
|
|
633
|
+
respondToolApproval(approvalId, decision) {
|
|
634
|
+
this.send({
|
|
635
|
+
type: "tool_approval_response",
|
|
636
|
+
approval_id: approvalId,
|
|
637
|
+
decision
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
respondPlanApproval(approvalId, decision, feedback) {
|
|
641
|
+
this.send({
|
|
642
|
+
type: "plan_approval_response",
|
|
643
|
+
approval_id: approvalId,
|
|
644
|
+
decision,
|
|
645
|
+
...(feedback && { feedback })
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
respondSecretRequest(approvalId, status) {
|
|
649
|
+
this.send({
|
|
650
|
+
type: "secret_request_response",
|
|
651
|
+
approval_id: approvalId,
|
|
652
|
+
status
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
respondToolResult(id, threadId, result, ok = true) {
|
|
656
|
+
this.send({
|
|
657
|
+
type: "tool_result",
|
|
658
|
+
tool_call_id: id,
|
|
659
|
+
thread_id: threadId,
|
|
660
|
+
result,
|
|
661
|
+
ok
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
setPermissionMode(threadId, mode) {
|
|
665
|
+
this.send({
|
|
666
|
+
command: "set_permission_mode",
|
|
667
|
+
data: { thread_id: threadId, permission_mode: mode }
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
disconnect() {
|
|
671
|
+
const ws = this.ws;
|
|
672
|
+
this.ws = null;
|
|
673
|
+
ws?.close();
|
|
674
|
+
this.drainWaiters();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
//# sourceMappingURL=websocket-client.js.map
|