@jxsuite/ai 0.1.0
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/dist/index.js +1531 -0
- package/dist/index.js.map +14 -0
- package/package.json +34 -0
- package/src/chat-state.ts +416 -0
- package/src/index.ts +21 -0
- package/src/streaming-client.ts +561 -0
- package/src/tools.ts +280 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-state.js — Provider-agnostic reactive chat state management
|
|
3
|
+
*
|
|
4
|
+
* Manages the lifecycle of a chat conversation: messages, streaming status,
|
|
5
|
+
* tool calls, and errors. Built on @vue/reactivity for fine-grained updates.
|
|
6
|
+
* No Studio or Jx dependencies — reusable in any chat UI context.
|
|
7
|
+
*
|
|
8
|
+
* @license MIT
|
|
9
|
+
* @module @jxsuite/ai/chat-state
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { reactive } from "@vue/reactivity";
|
|
13
|
+
|
|
14
|
+
import type { ToolResult } from "./tools.ts";
|
|
15
|
+
|
|
16
|
+
export type ChatState = "idle" | "streaming" | "error";
|
|
17
|
+
|
|
18
|
+
export type MessageRole = "user" | "assistant" | "system" | "tool";
|
|
19
|
+
|
|
20
|
+
export interface Message {
|
|
21
|
+
id: string;
|
|
22
|
+
role: MessageRole;
|
|
23
|
+
content: string;
|
|
24
|
+
toolCalls?: ToolCallRecord[];
|
|
25
|
+
toolCallId?: string;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ToolCallRecord {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
arguments: string;
|
|
33
|
+
result?: ToolResult | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ChatStore {
|
|
37
|
+
messages: Message[];
|
|
38
|
+
status: ChatState;
|
|
39
|
+
streamingContent: string;
|
|
40
|
+
pendingToolCalls: ToolCallRecord[];
|
|
41
|
+
error: string | null;
|
|
42
|
+
model: string;
|
|
43
|
+
tokenCount: number;
|
|
44
|
+
contextWarning: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
let _idCounter = 0;
|
|
52
|
+
|
|
53
|
+
/** @returns {string} */
|
|
54
|
+
function uid() {
|
|
55
|
+
_idCounter += 1;
|
|
56
|
+
return `msg_${Date.now()}_${_idCounter}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Factory ─────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a reactive chat state store.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @param {string} [opts.model] - Default model name
|
|
66
|
+
* @returns {ChatStore & {
|
|
67
|
+
* sendMessage: (text: string) => void;
|
|
68
|
+
* beginAssistantTurn: () => void;
|
|
69
|
+
* appendDelta: (content: string) => void;
|
|
70
|
+
* appendToolCallStart: (id: string, name: string) => void;
|
|
71
|
+
* appendToolCallDelta: (id: string, args: string) => void;
|
|
72
|
+
* appendToolCallEnd: (id: string) => void;
|
|
73
|
+
* appendToolResult: (id: string, result: ToolResult) => void;
|
|
74
|
+
* pushToolResultMessage: (toolCallId: string, content: string) => void;
|
|
75
|
+
* finishStream: (stopReason: string) => void;
|
|
76
|
+
* setError: (message: string) => void;
|
|
77
|
+
* cancelStream: () => void;
|
|
78
|
+
* clearChat: () => void;
|
|
79
|
+
* retryLast: () => void;
|
|
80
|
+
* setModel: (model: string) => void;
|
|
81
|
+
* setTokenCount: (count: number) => void;
|
|
82
|
+
* setContextWarning: (warning: boolean) => void;
|
|
83
|
+
* toMessagesArray: () => object[];
|
|
84
|
+
* }}
|
|
85
|
+
*/
|
|
86
|
+
export function createChatState(opts: { model?: string } = {}) {
|
|
87
|
+
const model = opts.model || "gpt-4o";
|
|
88
|
+
|
|
89
|
+
const store = reactive<ChatStore>({
|
|
90
|
+
messages: [],
|
|
91
|
+
status: "idle",
|
|
92
|
+
streamingContent: "",
|
|
93
|
+
pendingToolCalls: [],
|
|
94
|
+
error: null,
|
|
95
|
+
model,
|
|
96
|
+
tokenCount: 0,
|
|
97
|
+
contextWarning: false,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
let _streamingMessage: Message | null = null;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add a user message and prepare for streaming response.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} text
|
|
106
|
+
*/
|
|
107
|
+
function sendMessage(text: string) {
|
|
108
|
+
if (store.status === "streaming") {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const userMsg = {
|
|
113
|
+
id: uid(),
|
|
114
|
+
role: "user",
|
|
115
|
+
content: text,
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
} as Message;
|
|
118
|
+
store.messages.push(userMsg);
|
|
119
|
+
|
|
120
|
+
beginAssistantTurn();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Prepare for a new streaming assistant response without adding a user message. Used by the agent
|
|
125
|
+
* loop to start the next round after appending tool result messages.
|
|
126
|
+
*/
|
|
127
|
+
function beginAssistantTurn() {
|
|
128
|
+
if (store.status === "streaming") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
store.error = null;
|
|
133
|
+
store.streamingContent = "";
|
|
134
|
+
store.pendingToolCalls = [];
|
|
135
|
+
|
|
136
|
+
// Set status BEFORE pushing the placeholder so reactive effects see "streaming" when the
|
|
137
|
+
// Push triggers them — otherwise the effect renders the empty placeholder as a finalized msg.
|
|
138
|
+
store.status = "streaming";
|
|
139
|
+
|
|
140
|
+
// Create placeholder assistant message for streaming content
|
|
141
|
+
_streamingMessage = {
|
|
142
|
+
id: uid(),
|
|
143
|
+
role: "assistant",
|
|
144
|
+
content: "",
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
} as Message;
|
|
147
|
+
store.messages.push(_streamingMessage);
|
|
148
|
+
/*
|
|
149
|
+
* Re-read through the reactive proxy so appendDelta / appendToolCallStart mutations notify
|
|
150
|
+
* effects (e.g. watchAssistant in ai-panel.ts). _streamingMessage now holds the proxied
|
|
151
|
+
* version, not the raw object pushed above.
|
|
152
|
+
*/
|
|
153
|
+
_streamingMessage = store.messages.at(-1) ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Append a `tool` role message carrying a tool call's result, ready to be sent back to the LLM on
|
|
158
|
+
* the next round.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} toolCallId
|
|
161
|
+
* @param {string} content
|
|
162
|
+
*/
|
|
163
|
+
function pushToolResultMessage(toolCallId: string, content: string) {
|
|
164
|
+
store.messages.push({
|
|
165
|
+
id: uid(),
|
|
166
|
+
role: "tool",
|
|
167
|
+
toolCallId,
|
|
168
|
+
content,
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
} as Message);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Append a text delta to the streaming assistant message.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} content
|
|
177
|
+
*/
|
|
178
|
+
function appendDelta(content: string) {
|
|
179
|
+
if (store.status !== "streaming" || !_streamingMessage) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
store.streamingContent += content;
|
|
183
|
+
_streamingMessage.content = store.streamingContent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Start tracking a tool call within the stream.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} id
|
|
190
|
+
* @param {string} name
|
|
191
|
+
*/
|
|
192
|
+
function appendToolCallStart(id: string, name: string) {
|
|
193
|
+
if (store.status !== "streaming") {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const tc = {
|
|
197
|
+
id,
|
|
198
|
+
name,
|
|
199
|
+
arguments: "",
|
|
200
|
+
result: null,
|
|
201
|
+
} as ToolCallRecord;
|
|
202
|
+
store.pendingToolCalls.push(tc);
|
|
203
|
+
|
|
204
|
+
if (_streamingMessage) {
|
|
205
|
+
if (!_streamingMessage.toolCalls) {
|
|
206
|
+
_streamingMessage.toolCalls = [];
|
|
207
|
+
}
|
|
208
|
+
_streamingMessage.toolCalls.push(tc);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Append a partial argument fragment to a pending tool call.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} id
|
|
216
|
+
* @param {string} args
|
|
217
|
+
*/
|
|
218
|
+
function appendToolCallDelta(id: string, args: string) {
|
|
219
|
+
const tc = store.pendingToolCalls.find((t) => t.id === id);
|
|
220
|
+
if (tc) {
|
|
221
|
+
tc.arguments += args;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Mark a tool call as complete (all arguments received).
|
|
227
|
+
*
|
|
228
|
+
* @param {string} _id
|
|
229
|
+
*/
|
|
230
|
+
function appendToolCallEnd(_id: string) {
|
|
231
|
+
// Tool call is complete — arguments are fully accumulated.
|
|
232
|
+
// The caller should parse the arguments JSON and execute the tool.
|
|
233
|
+
// Tool results are attached via appendToolResult().
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Attach a result to a pending tool call.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} id
|
|
240
|
+
* @param {ToolResult} result
|
|
241
|
+
*/
|
|
242
|
+
function appendToolResult(id: string, result: ToolResult) {
|
|
243
|
+
const tc = store.pendingToolCalls.find((t) => t.id === id);
|
|
244
|
+
if (tc) {
|
|
245
|
+
tc.result = result;
|
|
246
|
+
}
|
|
247
|
+
// Also update in the message record
|
|
248
|
+
if (_streamingMessage?.toolCalls) {
|
|
249
|
+
const mtc = _streamingMessage.toolCalls.find((t) => t.id === id);
|
|
250
|
+
if (mtc) {
|
|
251
|
+
mtc.result = result;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Finalize the streaming response.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} _stopReason
|
|
260
|
+
*/
|
|
261
|
+
function finishStream(_stopReason: string) {
|
|
262
|
+
store.status = "idle";
|
|
263
|
+
store.streamingContent = "";
|
|
264
|
+
store.pendingToolCalls = [];
|
|
265
|
+
_streamingMessage = null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Set the chat state to error with a message.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} message
|
|
272
|
+
*/
|
|
273
|
+
function setError(message: string) {
|
|
274
|
+
store.status = "error";
|
|
275
|
+
store.error = message;
|
|
276
|
+
store.streamingContent = "";
|
|
277
|
+
// Remove the partial streaming message — it may contain incomplete tool_calls
|
|
278
|
+
// That would poison the conversation history on the next send.
|
|
279
|
+
if (_streamingMessage) {
|
|
280
|
+
const idx = store.messages.lastIndexOf(_streamingMessage);
|
|
281
|
+
if (idx !== -1) {
|
|
282
|
+
store.messages.splice(idx, 1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
_streamingMessage = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Cancel the current stream. Resets streaming state. */
|
|
289
|
+
function cancelStream() {
|
|
290
|
+
// Always remove the partial streaming message — it may contain
|
|
291
|
+
// Incomplete tool_calls that would poison the conversation history on retry.
|
|
292
|
+
if (_streamingMessage) {
|
|
293
|
+
const idx = store.messages.lastIndexOf(_streamingMessage);
|
|
294
|
+
if (idx !== -1) {
|
|
295
|
+
store.messages.splice(idx, 1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
store.status = "idle";
|
|
299
|
+
store.streamingContent = "";
|
|
300
|
+
store.pendingToolCalls = [];
|
|
301
|
+
_streamingMessage = null;
|
|
302
|
+
store.error = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Clear the entire chat history. */
|
|
306
|
+
function clearChat() {
|
|
307
|
+
store.messages.length = 0;
|
|
308
|
+
store.status = "idle";
|
|
309
|
+
store.streamingContent = "";
|
|
310
|
+
store.pendingToolCalls = [];
|
|
311
|
+
store.error = null;
|
|
312
|
+
store.contextWarning = false;
|
|
313
|
+
_streamingMessage = null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Retry the last user message (remove last assistant + user, then the caller re-sends). */
|
|
317
|
+
function retryLast() {
|
|
318
|
+
// Remove the last assistant message
|
|
319
|
+
while (store.messages.length > 0 && store.messages.at(-1)!.role !== "user") {
|
|
320
|
+
store.messages.pop();
|
|
321
|
+
}
|
|
322
|
+
// Remove the last user message (caller will re-send)
|
|
323
|
+
if (store.messages.length > 0 && store.messages.at(-1)!.role === "user") {
|
|
324
|
+
store.messages.pop();
|
|
325
|
+
}
|
|
326
|
+
store.status = "idle";
|
|
327
|
+
store.error = null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Set the model name.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} m
|
|
334
|
+
*/
|
|
335
|
+
function setModel(m: string) {
|
|
336
|
+
store.model = m;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Set the approximate token count for context management.
|
|
341
|
+
*
|
|
342
|
+
* @param {number} count
|
|
343
|
+
*/
|
|
344
|
+
function setTokenCount(count: number) {
|
|
345
|
+
store.tokenCount = count;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Set the context overflow warning flag.
|
|
350
|
+
*
|
|
351
|
+
* @param {boolean} warning
|
|
352
|
+
*/
|
|
353
|
+
function setContextWarning(warning: boolean) {
|
|
354
|
+
store.contextWarning = warning;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Convert the current chat state to an array suitable for sending to an LLM API. Includes the
|
|
359
|
+
* system prompt as the first message and all conversation turns.
|
|
360
|
+
*
|
|
361
|
+
* @returns {object[]}
|
|
362
|
+
*/
|
|
363
|
+
function toMessagesArray() {
|
|
364
|
+
const out = [];
|
|
365
|
+
|
|
366
|
+
for (const msg of store.messages) {
|
|
367
|
+
if (msg.role === "assistant" && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
368
|
+
out.push({
|
|
369
|
+
role: "assistant",
|
|
370
|
+
content: msg.content || null,
|
|
371
|
+
tool_calls: msg.toolCalls.map((tc) => ({
|
|
372
|
+
id: tc.id,
|
|
373
|
+
type: "function",
|
|
374
|
+
function: {
|
|
375
|
+
name: tc.name,
|
|
376
|
+
arguments: tc.arguments,
|
|
377
|
+
},
|
|
378
|
+
})),
|
|
379
|
+
});
|
|
380
|
+
} else if (msg.role === "tool") {
|
|
381
|
+
out.push({
|
|
382
|
+
role: "tool",
|
|
383
|
+
tool_call_id: msg.toolCallId,
|
|
384
|
+
content: msg.content,
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
out.push({
|
|
388
|
+
role: msg.role,
|
|
389
|
+
content: msg.content,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return Object.assign(store, {
|
|
398
|
+
sendMessage,
|
|
399
|
+
beginAssistantTurn,
|
|
400
|
+
appendDelta,
|
|
401
|
+
appendToolCallStart,
|
|
402
|
+
appendToolCallDelta,
|
|
403
|
+
appendToolCallEnd,
|
|
404
|
+
appendToolResult,
|
|
405
|
+
pushToolResultMessage,
|
|
406
|
+
finishStream,
|
|
407
|
+
setError,
|
|
408
|
+
cancelStream,
|
|
409
|
+
clearChat,
|
|
410
|
+
retryLast,
|
|
411
|
+
setModel,
|
|
412
|
+
setTokenCount,
|
|
413
|
+
setContextWarning,
|
|
414
|
+
toMessagesArray,
|
|
415
|
+
});
|
|
416
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index.js — AI infrastructure for Jx Suite (`@jxsuite/ai`)
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the core modules: streaming client abstraction, tool registry, and
|
|
5
|
+
* reactive chat state management. Provider-agnostic; no Studio or Jx dependencies.
|
|
6
|
+
*
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Re-export from sub-modules so consumers can `import { ... } from "@jxsuite/ai"`.
|
|
11
|
+
// Importing individual files instead enables tree-shaking.
|
|
12
|
+
export { createToolDefinition, createToolRegistry } from "./tools.js";
|
|
13
|
+
|
|
14
|
+
export { createChatState } from "./chat-state.js";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
STREAM_EVENT_TYPES,
|
|
18
|
+
createOpenAIStreamingClient,
|
|
19
|
+
createAnthropicStreamingClient,
|
|
20
|
+
createProxyStreamingClient,
|
|
21
|
+
} from "./streaming-client.js";
|