@yeaft/webchat-agent 0.1.576 → 0.1.578
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 +1 -1
- package/unify/threads/engine-instance.js +108 -16
package/package.json
CHANGED
|
@@ -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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
//
|
|
138
|
-
//
|
|
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
|
|
141
|
-
|
|
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
|
/**
|