@yeaft/webchat-agent 0.1.575 → 0.1.577

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.575",
3
+ "version": "0.1.577",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -111,8 +111,67 @@ export class EngineInstance {
111
111
  // Snapshot of messages passed to the engine; the engine treats this
112
112
  // as read-only (it builds its own conversation array internally).
113
113
  const snapshot = [...this.#messages];
114
- let assistantText = '';
115
- const assistantToolCalls = [];
114
+
115
+ // task-fix: chat-completions protocol requires every `tool_call_id`
116
+ // on an assistant message to be paired with a matching `role:'tool'`
117
+ // message in history. Without this, turn N+1 sends `tool_calls`
118
+ // orphaned from their results and OpenAI-compatible proxies return
119
+ // `invalid_request_body: No tool output found for function call`.
120
+ //
121
+ // A single query may contain MULTIPLE internal iterations (assistant
122
+ // → tools → assistant → tools → … → assistant-final). We mirror
123
+ // engine.js's own conversationMessages structure so the same
124
+ // interleaved pairing is preserved for subsequent turns:
125
+ //
126
+ // [user, assistant(text1, toolCalls1), tool r1a, tool r1b,
127
+ // assistant(text2, toolCalls2), tool r2a,
128
+ // assistant(finalText)]
129
+ //
130
+ // We flush one assistant message per `turn_end` boundary and
131
+ // append tool results as they stream in. Any assistant turn with
132
+ // toolCalls must have all its `role:'tool'` results paired before
133
+ // the NEXT assistant message (or placeholders — see below).
134
+ const newMessages = [];
135
+ let curText = '';
136
+ let curToolCalls = [];
137
+ let curToolResults = []; // buffered per-iteration, flushed AFTER assistant
138
+ const seenToolResults = new Set();
139
+
140
+ function flushAssistantTurn() {
141
+ // Emit the assistant message for the current iteration. Preserve
142
+ // an empty-content assistant (pure tool_calls) — some providers
143
+ // require content:'' rather than omission. The chat-completions
144
+ // adapter normalises either shape.
145
+ const assistantMsg = { role: 'assistant', content: curText };
146
+ if (curToolCalls.length > 0) {
147
+ assistantMsg.toolCalls = curToolCalls.map(tc => ({
148
+ id: tc.id, name: tc.name, input: tc.input,
149
+ }));
150
+ }
151
+ // Skip empty / no-op flushes (can happen on pre-first-turn boundaries).
152
+ if (curText || curToolCalls.length > 0) {
153
+ newMessages.push(assistantMsg);
154
+ }
155
+ // Any buffered tool results for THIS iteration must immediately
156
+ // follow the assistant that produced them — the adapter's history
157
+ // serialiser pairs by order-in-history.
158
+ for (const tr of curToolResults) newMessages.push(tr);
159
+ // Synthesize placeholders for unmatched toolCalls (abort paths).
160
+ for (const tc of curToolCalls) {
161
+ if (!seenToolResults.has(tc.id)) {
162
+ newMessages.push({
163
+ role: 'tool',
164
+ toolCallId: tc.id,
165
+ content: '[tool call did not produce a result — aborted or errored before completion]',
166
+ isError: true,
167
+ });
168
+ seenToolResults.add(tc.id);
169
+ }
170
+ }
171
+ curText = '';
172
+ curToolCalls = [];
173
+ curToolResults = [];
174
+ }
116
175
 
117
176
  for await (const event of this.#engine.query({ prompt, mode, messages: snapshot, signal })) {
118
177
  // Re-tag every event with the bound threadId. Non-object events
@@ -123,25 +182,58 @@ export class EngineInstance {
123
182
  : event;
124
183
  yield tagged;
125
184
 
126
- // Track assistant reply to persist after stream ends. Only tag the
127
- // natural stream types — not our injected turn_start/turn_end.
128
- if (event && typeof event === 'object') {
129
- if (event.type === 'text_delta' && typeof event.text === 'string') {
130
- assistantText += event.text;
131
- } else if (event.type === 'tool_call') {
132
- assistantToolCalls.push({ id: event.id, name: event.name, input: event.input });
133
- }
185
+ if (!event || typeof event !== 'object') continue;
186
+
187
+ switch (event.type) {
188
+ case 'text_delta':
189
+ if (typeof event.text === 'string') curText += event.text;
190
+ break;
191
+ case 'tool_call':
192
+ curToolCalls.push({ id: event.id, name: event.name, input: event.input });
193
+ break;
194
+ case 'tool_end':
195
+ if (event.id) {
196
+ // Mirror engine.js: tool result body is the `output` string;
197
+ // `isError:true` is carried forward. BUFFER here — flush
198
+ // places the assistant message FIRST, then these results,
199
+ // so the order [assistant(toolCalls), tool r1, tool r2]
200
+ // holds (required by OpenAI pairing rules).
201
+ const entry = {
202
+ role: 'tool',
203
+ toolCallId: event.id,
204
+ content: typeof event.output === 'string' ? event.output : String(event.output ?? ''),
205
+ };
206
+ if (event.isError) entry.isError = true;
207
+ curToolResults.push(entry);
208
+ seenToolResults.add(event.id);
209
+ }
210
+ break;
211
+ case 'turn_end':
212
+ // Boundary between internal iterations. engine.js order is:
213
+ // [text_delta*] [tool_call*] [tool_start tool_end]*
214
+ // then turn_end{stopReason:'tool_use'} (or 'end_turn')
215
+ // Flushing here writes the assistant message, then its
216
+ // buffered tool results, then placeholders for any orphans.
217
+ flushAssistantTurn();
218
+ break;
219
+ default:
220
+ break;
134
221
  }
135
222
  }
136
223
 
137
- // Append user + assistant to the owned messages array so subsequent
138
- // queries on this thread carry conversational context.
224
+ // Final safety flush if the engine terminated without a final
225
+ // turn_end (shouldn't happen in normal flows, but abort/error
226
+ // paths sometimes skip it), flush whatever we have.
227
+ if (curText || curToolCalls.length > 0 || curToolResults.length > 0) {
228
+ flushAssistantTurn();
229
+ }
230
+
231
+ // Append user + all captured messages to the owned array so
232
+ // subsequent queries on this thread carry conversational context.
139
233
  this.#messages.push({ role: 'user', content: prompt });
140
- const assistantMsg = { role: 'assistant', content: assistantText };
141
- if (assistantToolCalls.length > 0) {
142
- assistantMsg.toolCalls = assistantToolCalls;
234
+ for (const m of newMessages) {
235
+ this.#messages.push(m);
143
236
  }
144
- this.#messages.push(assistantMsg);
145
237
  }
146
238
 
147
239
  /**