@yeaft/webchat-agent 0.1.804 → 0.1.805
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/conversation/persist.js +65 -0
- package/unify/effort.js +6 -5
- package/unify/engine.js +28 -0
- package/unify/llm/adapter.js +5 -2
- package/unify/llm/anthropic.js +91 -14
- package/unify/web-bridge.js +41 -3
package/package.json
CHANGED
|
@@ -142,6 +142,33 @@ function serializeMessage(msg) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// task-327d: persist Anthropic extended-thinking blocks so the next turn
|
|
146
|
+
// can echo them back with their server-signed signature. Both fields are
|
|
147
|
+
// base64'd: thinking is multi-line text, and the signature is opaque
|
|
148
|
+
// bytes that don't need to be human-readable. Without this round-trip
|
|
149
|
+
// the next Anthropic request 400s with "content[].thinking in the
|
|
150
|
+
// thinking mode must be passed back to the API".
|
|
151
|
+
if (msg.thinkingBlocks && msg.thinkingBlocks.length > 0) {
|
|
152
|
+
fm.push(`thinkingBlocks:`);
|
|
153
|
+
for (const tb of msg.thinkingBlocks) {
|
|
154
|
+
if (!tb || typeof tb.signature !== 'string' || !tb.signature) continue;
|
|
155
|
+
if (tb.redacted) {
|
|
156
|
+
if (typeof tb.data !== 'string') continue;
|
|
157
|
+
const dataB64 = Buffer.from(tb.data, 'utf8').toString('base64');
|
|
158
|
+
const signatureB64 = Buffer.from(tb.signature, 'utf8').toString('base64');
|
|
159
|
+
fm.push(` - redacted: true`);
|
|
160
|
+
fm.push(` dataB64: ${dataB64}`);
|
|
161
|
+
fm.push(` signatureB64: ${signatureB64}`);
|
|
162
|
+
} else {
|
|
163
|
+
if (typeof tb.thinking !== 'string') continue;
|
|
164
|
+
const thinkingB64 = Buffer.from(tb.thinking, 'utf8').toString('base64');
|
|
165
|
+
const signatureB64 = Buffer.from(tb.signature, 'utf8').toString('base64');
|
|
166
|
+
fm.push(` - thinkingB64: ${thinkingB64}`);
|
|
167
|
+
fm.push(` signatureB64: ${signatureB64}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
145
172
|
fm.push('---');
|
|
146
173
|
fm.push('');
|
|
147
174
|
fm.push(content);
|
|
@@ -229,6 +256,44 @@ export function parseMessage(raw) {
|
|
|
229
256
|
if (toolCalls.length > 0) msg.toolCalls = toolCalls;
|
|
230
257
|
}
|
|
231
258
|
|
|
259
|
+
// task-327d: parse thinkingBlocks (mirror of toolCalls parser above)
|
|
260
|
+
if (frontmatter.includes('thinkingBlocks:')) {
|
|
261
|
+
const thinkingBlocks = [];
|
|
262
|
+
const tbMatch = frontmatter.match(/thinkingBlocks:\n((?:\s+-\s+[\s\S]*?)(?=\n\w|$))/);
|
|
263
|
+
if (tbMatch) {
|
|
264
|
+
const tbBlock = tbMatch[1];
|
|
265
|
+
const entries = tbBlock.split(/\n\s+-\s+/).filter(Boolean);
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
const tb = {};
|
|
268
|
+
for (const line of entry.split('\n')) {
|
|
269
|
+
const trimmed = line.trim().replace(/^-\s+/, '');
|
|
270
|
+
const ci = trimmed.indexOf(':');
|
|
271
|
+
if (ci === -1) continue;
|
|
272
|
+
const k = trimmed.slice(0, ci).trim();
|
|
273
|
+
const v = trimmed.slice(ci + 1).trim();
|
|
274
|
+
if (k === 'thinkingB64') {
|
|
275
|
+
tb.thinking = Buffer.from(v, 'base64').toString('utf8');
|
|
276
|
+
} else if (k === 'dataB64') {
|
|
277
|
+
tb.data = Buffer.from(v, 'base64').toString('utf8');
|
|
278
|
+
} else if (k === 'signatureB64') {
|
|
279
|
+
tb.signature = Buffer.from(v, 'base64').toString('utf8');
|
|
280
|
+
} else if (k === 'redacted') {
|
|
281
|
+
tb.redacted = v === 'true';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Both fields required — an unsigned block would 400 on replay.
|
|
285
|
+
if (tb.redacted) {
|
|
286
|
+
if (typeof tb.data === 'string' && typeof tb.signature === 'string' && tb.signature) {
|
|
287
|
+
thinkingBlocks.push(tb);
|
|
288
|
+
}
|
|
289
|
+
} else if (typeof tb.thinking === 'string' && typeof tb.signature === 'string' && tb.signature) {
|
|
290
|
+
thinkingBlocks.push(tb);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (thinkingBlocks.length > 0) msg.thinkingBlocks = thinkingBlocks;
|
|
295
|
+
}
|
|
296
|
+
|
|
232
297
|
return msg;
|
|
233
298
|
}
|
|
234
299
|
|
package/unify/effort.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 4. null (no effort = adapter/router drops the param)
|
|
14
14
|
*
|
|
15
15
|
* Red lines:
|
|
16
|
-
* • Never error on unknown scenario — default to '
|
|
16
|
+
* • Never error on unknown scenario — default to 'max'.
|
|
17
17
|
* • Feature flag UNIFY_THINKING_V1 is enforced at the adapter/router
|
|
18
18
|
* layer; this module just computes the intended value. If the flag
|
|
19
19
|
* is off, adapters drop it anyway.
|
|
@@ -36,7 +36,8 @@ export const LONG_LOOP_TURN_THRESHOLD = 8;
|
|
|
36
36
|
* a scenario string before invoking `pickEffort()`.
|
|
37
37
|
*
|
|
38
38
|
* Tiers (6 scenarios per architect spec):
|
|
39
|
-
* chat →
|
|
39
|
+
* chat → max (default interactive pair-programming turn —
|
|
40
|
+
* quality over latency; per user 2026-05-22)
|
|
40
41
|
* consolidate → max (memory compaction — quality matters, runs once)
|
|
41
42
|
* dream → max (memory maintenance — same rationale)
|
|
42
43
|
* sub_agent → max (coordinator spawns + merges)
|
|
@@ -47,7 +48,7 @@ export const LONG_LOOP_TURN_THRESHOLD = 8;
|
|
|
47
48
|
* Unknown scenarios fall through to 'high'.
|
|
48
49
|
*/
|
|
49
50
|
export const SCENARIO_EFFORT = Object.freeze({
|
|
50
|
-
chat: '
|
|
51
|
+
chat: 'max',
|
|
51
52
|
consolidate: 'max',
|
|
52
53
|
dream: 'max',
|
|
53
54
|
sub_agent: 'max',
|
|
@@ -65,7 +66,7 @@ export const SCENARIO_EFFORT = Object.freeze({
|
|
|
65
66
|
* `/max` prefix, Settings slider, or API caller.
|
|
66
67
|
* 2. If toolLoopTurns >= LONG_LOOP_TURN_THRESHOLD, upgrade the
|
|
67
68
|
* base scenario to 'long_loop' (→ 'max').
|
|
68
|
-
* 3. Look up SCENARIO_EFFORT[scenario]; unknown → '
|
|
69
|
+
* 3. Look up SCENARIO_EFFORT[scenario]; unknown → 'max'.
|
|
69
70
|
*
|
|
70
71
|
* @param {object} ctx
|
|
71
72
|
* @param {string} [ctx.scenario='chat'] — Scenario tag; see SCENARIO_EFFORT.
|
|
@@ -92,7 +93,7 @@ export function pickEffort({ scenario = 'chat', toolLoopTurns = 0, userEffort =
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// 3. Scenario table lookup.
|
|
95
|
-
return SCENARIO_EFFORT[scenario] || '
|
|
96
|
+
return SCENARIO_EFFORT[scenario] || 'max';
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/**
|
package/unify/engine.js
CHANGED
|
@@ -1486,6 +1486,7 @@ export class Engine {
|
|
|
1486
1486
|
let ttfbMs = null; // Time to first token
|
|
1487
1487
|
let responseText = '';
|
|
1488
1488
|
const toolCalls = [];
|
|
1489
|
+
const thinkingBlocks = []; // task-327d: collected from adapter for round-trip
|
|
1489
1490
|
let stopReason = 'end_turn';
|
|
1490
1491
|
const totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1491
1492
|
// task-344: capture redacted raw request / raw response for debug panel.
|
|
@@ -1660,6 +1661,22 @@ export class Engine {
|
|
|
1660
1661
|
case 'thinking_delta':
|
|
1661
1662
|
yield event;
|
|
1662
1663
|
break;
|
|
1664
|
+
case 'thinking_block_end':
|
|
1665
|
+
// task-327d: collect server-signed thinking block for
|
|
1666
|
+
// round-trip replay. Anthropic 400s the next turn if a
|
|
1667
|
+
// thinking block (regular or redacted) was emitted but not
|
|
1668
|
+
// echoed back with its original signature. Drop blocks
|
|
1669
|
+
// missing a signature — replay-without-sig 400s identically.
|
|
1670
|
+
if (event.signature) {
|
|
1671
|
+
if (event.redacted) {
|
|
1672
|
+
thinkingBlocks.push({ redacted: true, data: event.data, signature: event.signature });
|
|
1673
|
+
} else {
|
|
1674
|
+
thinkingBlocks.push({ thinking: event.thinking, signature: event.signature });
|
|
1675
|
+
}
|
|
1676
|
+
} else {
|
|
1677
|
+
console.warn('[Engine] thinking block missing signature — dropping; next turn would 400 on replay');
|
|
1678
|
+
}
|
|
1679
|
+
break;
|
|
1663
1680
|
case 'tool_call':
|
|
1664
1681
|
toolCalls.push(event);
|
|
1665
1682
|
yield event;
|
|
@@ -1843,6 +1860,17 @@ export class Engine {
|
|
|
1843
1860
|
input: tc.input,
|
|
1844
1861
|
}));
|
|
1845
1862
|
}
|
|
1863
|
+
// task-327d: persist thinking blocks for the next turn's replay.
|
|
1864
|
+
// Anthropic requires assistant.thinking blocks to be echoed back
|
|
1865
|
+
// verbatim (text + signature) when the previous turn used extended
|
|
1866
|
+
// thinking — see translateMessages in anthropic.js.
|
|
1867
|
+
if (thinkingBlocks.length > 0) {
|
|
1868
|
+
assistantMsg.thinkingBlocks = thinkingBlocks.map(tb => (
|
|
1869
|
+
tb.redacted
|
|
1870
|
+
? { redacted: true, data: tb.data, signature: tb.signature }
|
|
1871
|
+
: { thinking: tb.thinking, signature: tb.signature }
|
|
1872
|
+
));
|
|
1873
|
+
}
|
|
1846
1874
|
// Phase 8 (DESIGN.md §9.15): carry the router plan back on the
|
|
1847
1875
|
// assistant message that produced it. Stripped at the wire by
|
|
1848
1876
|
// stripMetaForWire — pure bookkeeping for priorPlan continuity.
|
package/unify/llm/adapter.js
CHANGED
|
@@ -40,20 +40,23 @@
|
|
|
40
40
|
/**
|
|
41
41
|
* @typedef {{ type: 'text_delta', text: string }} TextDeltaEvent
|
|
42
42
|
* @typedef {{ type: 'thinking_delta', text: string }} ThinkingDeltaEvent
|
|
43
|
+
* @typedef {{ type: 'thinking_block_end', thinking: string, signature: string }} ThinkingBlockEndEvent
|
|
43
44
|
* @typedef {{ type: 'tool_call', id: string, name: string, input: object }} ToolCallEvent
|
|
44
45
|
* @typedef {{ type: 'usage', inputTokens: number, outputTokens: number, cacheReadTokens?: number, cacheWriteTokens?: number }} UsageEvent
|
|
45
46
|
* @typedef {{ type: 'stop', stopReason: 'end_turn' | 'tool_use' | 'max_tokens' }} StopEvent
|
|
46
47
|
* @typedef {{ type: 'error', error: Error, retryable: boolean }} ErrorEvent
|
|
47
48
|
*
|
|
48
|
-
* @typedef {TextDeltaEvent | ThinkingDeltaEvent | ToolCallEvent | UsageEvent | StopEvent | ErrorEvent} StreamEvent
|
|
49
|
+
* @typedef {TextDeltaEvent | ThinkingDeltaEvent | ThinkingBlockEndEvent | ToolCallEvent | UsageEvent | StopEvent | ErrorEvent} StreamEvent
|
|
49
50
|
*/
|
|
50
51
|
|
|
51
52
|
// ─── Unified Message Types ─────────────────────────────────────
|
|
52
53
|
|
|
53
54
|
/**
|
|
55
|
+
* @typedef {{ thinking: string, signature: string }} ThinkingBlock
|
|
56
|
+
*
|
|
54
57
|
* @typedef {{ role: 'system', content: string }} SystemMessage
|
|
55
58
|
* @typedef {{ role: 'user', content: string }} UserMessage
|
|
56
|
-
* @typedef {{ role: 'assistant', content: string, toolCalls?: UnifiedToolCall[] }} AssistantMessage
|
|
59
|
+
* @typedef {{ role: 'assistant', content: string, toolCalls?: UnifiedToolCall[], thinkingBlocks?: ThinkingBlock[] }} AssistantMessage
|
|
57
60
|
* @typedef {{ role: 'tool', toolCallId: string, content: string, isError?: boolean }} ToolMessage
|
|
58
61
|
*
|
|
59
62
|
* @typedef {SystemMessage | UserMessage | AssistantMessage | ToolMessage} UnifiedMessage
|
package/unify/llm/anthropic.js
CHANGED
|
@@ -73,6 +73,24 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
73
73
|
result.push({ role: 'user', content: msg.content });
|
|
74
74
|
} else if (msg.role === 'assistant') {
|
|
75
75
|
const content = [];
|
|
76
|
+
// task-327d: Anthropic requires thinking blocks to appear BEFORE
|
|
77
|
+
// any text / tool_use in the content array on echo-back. When the
|
|
78
|
+
// previous turn produced thinking blocks (with server-signed
|
|
79
|
+
// signature), we MUST replay them verbatim or the next request
|
|
80
|
+
// 400s with "content[].thinking in the thinking mode must be
|
|
81
|
+
// passed back to the API". Order is mandatory.
|
|
82
|
+
if (Array.isArray(msg.thinkingBlocks)) {
|
|
83
|
+
for (const tb of msg.thinkingBlocks) {
|
|
84
|
+
if (!tb || typeof tb.signature !== 'string' || !tb.signature) continue;
|
|
85
|
+
if (tb.redacted) {
|
|
86
|
+
if (typeof tb.data !== 'string') continue;
|
|
87
|
+
content.push({ type: 'redacted_thinking', data: tb.data, signature: tb.signature });
|
|
88
|
+
} else {
|
|
89
|
+
if (typeof tb.thinking !== 'string') continue;
|
|
90
|
+
content.push({ type: 'thinking', thinking: tb.thinking, signature: tb.signature });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
76
94
|
if (msg.content) {
|
|
77
95
|
content.push({ type: 'text', text: msg.content });
|
|
78
96
|
}
|
|
@@ -216,9 +234,16 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
216
234
|
const reader = response.body.getReader();
|
|
217
235
|
const decoder = new TextDecoder();
|
|
218
236
|
let buffer = '';
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
237
|
+
// task-327d: index-keyed per-block state. Anthropic streams content
|
|
238
|
+
// blocks sequentially today, but the protocol exposes `event.index`
|
|
239
|
+
// precisely because that's not guaranteed. Dispatch in
|
|
240
|
+
// content_block_stop must look up by index, never "whichever scalar
|
|
241
|
+
// happens to still be set." States by kind: 'tool_use', 'thinking',
|
|
242
|
+
// 'redacted_thinking'. Redacted blocks carry opaque `data` instead
|
|
243
|
+
// of `thinking` text but share the same echo-back rule (drop without
|
|
244
|
+
// signature → next turn 400s identically).
|
|
245
|
+
/** @type {Map<number, { kind: string, [k: string]: any }>} */
|
|
246
|
+
const blockByIndex = new Map();
|
|
222
247
|
// Accumulate raw SSE body verbatim for the debug panel. No truncation:
|
|
223
248
|
// see `redactRawRequest` in adapter.js for the verbatim-design rationale.
|
|
224
249
|
// Push-then-join keeps allocation bounded for multi-MiB payloads (avoids
|
|
@@ -254,38 +279,90 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
254
279
|
|
|
255
280
|
if (type === 'content_block_start') {
|
|
256
281
|
const block = event.content_block;
|
|
282
|
+
const idx = event.index;
|
|
257
283
|
if (block?.type === 'tool_use') {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
blockByIndex.set(idx, {
|
|
285
|
+
kind: 'tool_use',
|
|
286
|
+
id: block.id,
|
|
287
|
+
name: block.name,
|
|
288
|
+
input: '',
|
|
289
|
+
});
|
|
290
|
+
} else if (block?.type === 'thinking') {
|
|
291
|
+
blockByIndex.set(idx, {
|
|
292
|
+
kind: 'thinking',
|
|
293
|
+
thinking: typeof block.thinking === 'string' ? block.thinking : '',
|
|
294
|
+
signature: typeof block.signature === 'string' ? block.signature : '',
|
|
295
|
+
});
|
|
296
|
+
} else if (block?.type === 'redacted_thinking') {
|
|
297
|
+
// task-327d: API-redacted thinking. Body is opaque `data`
|
|
298
|
+
// (server-encrypted, not user-readable); we still need to
|
|
299
|
+
// echo it back with signature on the next turn or the API
|
|
300
|
+
// 400s with the same "must be passed back" error.
|
|
301
|
+
blockByIndex.set(idx, {
|
|
302
|
+
kind: 'redacted_thinking',
|
|
303
|
+
data: typeof block.data === 'string' ? block.data : '',
|
|
304
|
+
signature: typeof block.signature === 'string' ? block.signature : '',
|
|
305
|
+
});
|
|
261
306
|
}
|
|
262
307
|
} else if (type === 'content_block_delta') {
|
|
263
308
|
const delta = event.delta;
|
|
309
|
+
const idx = event.index;
|
|
310
|
+
const st = blockByIndex.get(idx);
|
|
264
311
|
if (delta?.type === 'text_delta') {
|
|
265
312
|
yield { type: 'text_delta', text: delta.text };
|
|
266
313
|
} else if (delta?.type === 'thinking_delta') {
|
|
314
|
+
// Forward delta for live UI; ALSO accumulate for round-trip.
|
|
315
|
+
if (st && st.kind === 'thinking') st.thinking += delta.thinking || '';
|
|
267
316
|
yield { type: 'thinking_delta', text: delta.thinking };
|
|
317
|
+
} else if (delta?.type === 'signature_delta') {
|
|
318
|
+
// Anthropic typically sends signature in one delta near the
|
|
319
|
+
// end of the (redacted_)thinking block. Accumulate defensively.
|
|
320
|
+
if (st && (st.kind === 'thinking' || st.kind === 'redacted_thinking')) {
|
|
321
|
+
st.signature += delta.signature || '';
|
|
322
|
+
}
|
|
268
323
|
} else if (delta?.type === 'input_json_delta') {
|
|
269
|
-
|
|
324
|
+
if (st && st.kind === 'tool_use') st.input += delta.partial_json;
|
|
270
325
|
}
|
|
271
326
|
} else if (type === 'content_block_stop') {
|
|
272
|
-
|
|
327
|
+
const idx = event.index;
|
|
328
|
+
const st = blockByIndex.get(idx);
|
|
329
|
+
if (!st) {
|
|
330
|
+
// Unknown / unhandled block kind (e.g. text — we don't track
|
|
331
|
+
// text state because text_delta is forwarded immediately).
|
|
332
|
+
} else if (st.kind === 'tool_use') {
|
|
273
333
|
let parsedInput = {};
|
|
274
334
|
try {
|
|
275
|
-
parsedInput =
|
|
335
|
+
parsedInput = st.input ? JSON.parse(st.input) : {};
|
|
276
336
|
} catch {
|
|
277
337
|
parsedInput = {};
|
|
278
338
|
}
|
|
279
339
|
yield {
|
|
280
340
|
type: 'tool_call',
|
|
281
|
-
id:
|
|
282
|
-
name:
|
|
341
|
+
id: st.id,
|
|
342
|
+
name: st.name,
|
|
283
343
|
input: parsedInput,
|
|
284
344
|
};
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
345
|
+
} else if (st.kind === 'thinking' || st.kind === 'redacted_thinking') {
|
|
346
|
+
// task-327d: emit ONE end-of-block event with the assembled
|
|
347
|
+
// payload + signature. Engine collects these for replay.
|
|
348
|
+
// We emit even when signature is empty so engine can
|
|
349
|
+
// warn-and-drop; replaying without signature would 400.
|
|
350
|
+
if (st.kind === 'thinking') {
|
|
351
|
+
yield {
|
|
352
|
+
type: 'thinking_block_end',
|
|
353
|
+
thinking: st.thinking,
|
|
354
|
+
signature: st.signature,
|
|
355
|
+
};
|
|
356
|
+
} else {
|
|
357
|
+
yield {
|
|
358
|
+
type: 'thinking_block_end',
|
|
359
|
+
redacted: true,
|
|
360
|
+
data: st.data,
|
|
361
|
+
signature: st.signature,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
288
364
|
}
|
|
365
|
+
blockByIndex.delete(idx);
|
|
289
366
|
} else if (type === 'message_delta') {
|
|
290
367
|
const stopReason = event.delta?.stop_reason;
|
|
291
368
|
if (stopReason) {
|
package/unify/web-bridge.js
CHANGED
|
@@ -1651,7 +1651,7 @@ function maybeTransitionVpStatus(hctx, state) {
|
|
|
1651
1651
|
* todos, debug cards, and persistence all share the same boundary.
|
|
1652
1652
|
*
|
|
1653
1653
|
* @param {object} event — engine event (text_delta / tool_call / …)
|
|
1654
|
-
* @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, resetQueryTimer:Function, groupId?:string, vpId?:string, turnId?:string}} hctx
|
|
1654
|
+
* @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, thinkingBlocksAccum?:Array, resetQueryTimer:Function, groupId?:string, vpId?:string, turnId?:string}} hctx
|
|
1655
1655
|
*/
|
|
1656
1656
|
function handleEngineEvent(event, hctx) {
|
|
1657
1657
|
hctx.resetQueryTimer();
|
|
@@ -1679,6 +1679,30 @@ function handleEngineEvent(event, hctx) {
|
|
|
1679
1679
|
sendUnifyEvent({ type: 'thinking_delta', text: event.text }, envelope);
|
|
1680
1680
|
break;
|
|
1681
1681
|
|
|
1682
|
+
case 'thinking_block_end':
|
|
1683
|
+
// task-327d: capture the assembled thinking block (with server-
|
|
1684
|
+
// signed signature) so the group history we hand to subsequent
|
|
1685
|
+
// turns / VPs includes it. Without this echo Anthropic 400s the
|
|
1686
|
+
// next request with "content[].thinking in the thinking mode must
|
|
1687
|
+
// be passed back to the API". The signature stays server-side
|
|
1688
|
+
// only — wire serializers (stripMetaForWire / sendUnifyOutput)
|
|
1689
|
+
// never reference thinkingBlocks, so it cannot leak to the UI.
|
|
1690
|
+
if (hctx.thinkingBlocksAccum && event.signature) {
|
|
1691
|
+
if (event.redacted) {
|
|
1692
|
+
hctx.thinkingBlocksAccum.push({
|
|
1693
|
+
redacted: true,
|
|
1694
|
+
data: event.data,
|
|
1695
|
+
signature: event.signature,
|
|
1696
|
+
});
|
|
1697
|
+
} else {
|
|
1698
|
+
hctx.thinkingBlocksAccum.push({
|
|
1699
|
+
thinking: event.thinking,
|
|
1700
|
+
signature: event.signature,
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
break;
|
|
1705
|
+
|
|
1682
1706
|
case 'tool_call':
|
|
1683
1707
|
// Capture tool_call for the assistant message's toolCalls array so
|
|
1684
1708
|
// the next turn's history pairs `tool_calls` with `role:'tool'`
|
|
@@ -2534,6 +2558,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
|
|
|
2534
2558
|
const assistantTextParts = [];
|
|
2535
2559
|
const toolCallsAccum = [];
|
|
2536
2560
|
const toolResultsAccum = [];
|
|
2561
|
+
const thinkingBlocksAccum = []; // task-327d: round-trip to next turn
|
|
2537
2562
|
const appendedUserPrompts = [];
|
|
2538
2563
|
let vpEngine = null;
|
|
2539
2564
|
|
|
@@ -2559,6 +2584,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
|
|
|
2559
2584
|
assistantTextParts,
|
|
2560
2585
|
toolCallsAccum,
|
|
2561
2586
|
toolResultsAccum,
|
|
2587
|
+
thinkingBlocksAccum,
|
|
2562
2588
|
resetQueryTimer,
|
|
2563
2589
|
groupId,
|
|
2564
2590
|
vpId,
|
|
@@ -2599,7 +2625,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
|
|
|
2599
2625
|
}
|
|
2600
2626
|
|
|
2601
2627
|
// Turn completed — atomically append this VP's output to shared history.
|
|
2602
|
-
appendTurnToGroupHistory(groupId, threadId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum);
|
|
2628
|
+
appendTurnToGroupHistory(groupId, threadId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
|
|
2603
2629
|
|
|
2604
2630
|
sendUnifyOutput({
|
|
2605
2631
|
type: 'assistant',
|
|
@@ -2705,7 +2731,7 @@ async function runVpTurn({ prompt, promptParts = null, groupId, vpId, threadId =
|
|
|
2705
2731
|
* a session, this in-memory tape carries the un-collapsed form — which
|
|
2706
2732
|
* is fine because each VP turn's `engine.query` re-collapses on the fly.
|
|
2707
2733
|
*/
|
|
2708
|
-
function appendTurnToGroupHistory(groupId, threadId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum) {
|
|
2734
|
+
function appendTurnToGroupHistory(groupId, threadId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum) {
|
|
2709
2735
|
if (!groupId) return;
|
|
2710
2736
|
const history = getOrCreateGroupHistory(groupId);
|
|
2711
2737
|
const promptList = Array.isArray(prompts) ? prompts : [prompts];
|
|
@@ -2725,6 +2751,18 @@ function appendTurnToGroupHistory(groupId, threadId, prompts, assistantTextParts
|
|
|
2725
2751
|
input: tc.input,
|
|
2726
2752
|
}));
|
|
2727
2753
|
}
|
|
2754
|
+
// task-327d: carry thinking blocks across turns. Anthropic protocol
|
|
2755
|
+
// requires us to echo them back on the next request or the API
|
|
2756
|
+
// returns "content[].thinking in the thinking mode must be passed
|
|
2757
|
+
// back to the API". The signature is server-private — it stays in
|
|
2758
|
+
// this in-memory history and in agent-side persistence only.
|
|
2759
|
+
if (Array.isArray(thinkingBlocksAccum) && thinkingBlocksAccum.length > 0) {
|
|
2760
|
+
assistantMsg.thinkingBlocks = thinkingBlocksAccum.map(tb => (
|
|
2761
|
+
tb.redacted
|
|
2762
|
+
? { redacted: true, data: tb.data, signature: tb.signature }
|
|
2763
|
+
: { thinking: tb.thinking, signature: tb.signature }
|
|
2764
|
+
));
|
|
2765
|
+
}
|
|
2728
2766
|
history.push(assistantMsg);
|
|
2729
2767
|
|
|
2730
2768
|
for (const tr of toolResultsAccum) {
|