@openchambery/web 1.19.15-beta.4 → 1.19.15
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/assets/{AssistantView-bxT9mr97.js → AssistantView-xdL1EvpR.js} +2 -2
- package/dist/assets/{DiagramView-DJPrdklU.js → DiagramView-DPgkYU-_.js} +1 -1
- package/dist/assets/MarkdownRenderer-CP7UVZdr.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-CBdwjEMH.js → MarkdownRendererImpl-BB1Q1RWk.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-CHwYXkzv.js → MarkstreamRendererImpl-BjQrUIcq.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DLmATOi0.js → MobileDetailNavigation-Cyzs6cuR.js} +1 -1
- package/dist/assets/{MobileShareBridge-BbvoAeXg.js → MobileShareBridge-A-Z6jMUn.js} +1 -1
- package/dist/assets/MobileSurface-DjDa20gx.js +2 -0
- package/dist/assets/{MultiRunWindow-B7XPMkdc.js → MultiRunWindow-Cg5x-kJV.js} +1 -1
- package/dist/assets/{SettingsView-DABJm3u8.js → SettingsView-CYrTHDdp.js} +1 -1
- package/dist/assets/{SettingsWindow-D7n0m-52.js → SettingsWindow-CF-0Zgxr.js} +1 -1
- package/dist/assets/{TerminalView-DERnAkUi.js → TerminalView-CShou18D.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Bx3plCCc.js → ToolOutputDialog-BvmxlNOa.js} +1 -1
- package/dist/assets/{appThemeRegistry-BdHiNL-J.js → appThemeRegistry-iqSTUpPj.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-B3sIMQCF.js → fileMentionTouchSelection-CddGH2aH.js} +1 -1
- package/dist/assets/{gitApi-CDc5ynsh.js → gitApi-BwuaHHRQ.js} +1 -1
- package/dist/assets/{insertionBoundaries-3yNePddt.js → insertionBoundaries-og6NBgdn.js} +1 -1
- package/dist/assets/{main-5GGJlyEB.js → main-31g3JiQx.js} +4 -4
- package/dist/assets/{main-C_21O0IU.js → main-VD-QBM47.js} +2 -2
- package/dist/assets/{miniChat-Ti_tFK96.js → miniChat-DwCMTamY.js} +2 -2
- package/dist/assets/{mobile-BUYP8DBL.js → mobile-C7q1yowe.js} +2 -2
- package/dist/assets/{multirun-CXazK6qX.js → multirun-DcN5T0lN.js} +1 -1
- package/dist/assets/{projectMeta-sXx-r5jn.js → projectMeta-moNJYCK4.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-CLx8jNP4.js → renderElectronMiniChatApp-DVkgKA6A.js} +2 -2
- package/dist/assets/{renderMobileApp-BGKxwMFU.js → renderMobileApp-BgEXZhCO.js} +4 -4
- package/dist/assets/{runtimeConfig-YCy1Bcb2.js → runtimeConfig-B49cxtgI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-Dh3YvmRM.js → runtimeEndpointReset-9K_Dj1qR.js} +1 -1
- package/dist/assets/{sessionLookup-BzkA2pF0.js → sessionLookup-m3h8c4xR.js} +1 -1
- package/dist/assets/{useAppFontEffects-BLw4Av7h.js → useAppFontEffects-DRFi4TCE.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-DNDIYvwP.js → useDesktopWindowControlsLayout-BdNR3zMN.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-B_3sINMx.js → useEffectiveDirectory-COoPja_2.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-BBVnSn9p.js → useMobileNavigationStore-Cn0-ozUB.js} +1 -1
- package/dist/assets/{useSessionAutoCleanup-SsGxsSRB.js → useSessionAutoCleanup-XwgyzsEd.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-WCuVI6O_.js → useWorkerHighlightedLines-GgcqEoKY.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +31 -6
- package/server/lib/assistants/contact-tools.js +1 -16
- package/server/lib/assistants/contact-tools.test.js +0 -27
- package/server/lib/assistants/harness.js +140 -133
- package/server/lib/assistants/harness.test.js +207 -43
- package/server/lib/assistants/service.js +10 -33
- package/server/lib/assistants/service.test.js +75 -3
- package/server/lib/llm/DOCUMENTATION.md +8 -1
- package/server/lib/llm/completions.js +5 -0
- package/server/lib/llm/completions.test.js +19 -0
- package/server/lib/llm/generate.js +5 -1
- package/server/lib/llm/generate.test.js +23 -0
- package/dist/assets/MarkdownRenderer-4gjE7aM5.js +0 -2
- package/dist/assets/MobileSurface-BBOx43VU.js +0 -2
|
@@ -1,35 +1,17 @@
|
|
|
1
1
|
import { Agent } from '@earendil-works/pi-agent-core';
|
|
2
2
|
import { isContactSpokenPreamble, splitContactBubbles } from './bubbles.js';
|
|
3
3
|
import {
|
|
4
|
-
ASSIGN_SESSION_TOOL_NAME,
|
|
5
|
-
CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
6
|
-
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
7
|
-
confirmBubbleAfterContactReset,
|
|
8
4
|
contactTurnClearedChatHistory,
|
|
9
5
|
contactTurnHasSuccessfulReset,
|
|
10
6
|
contactTurnHasToolResult,
|
|
11
|
-
CREATE_ASSISTANT_TOOL_NAME,
|
|
12
7
|
detectRequestedContactTools,
|
|
13
8
|
extractContactCardsFromMessages,
|
|
14
9
|
formatContactToolsPrompt,
|
|
15
10
|
formatConnectedModelsPrompt,
|
|
16
11
|
formatRegisteredProjectsPrompt,
|
|
17
|
-
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
18
|
-
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
19
12
|
MISSED_FENCE_RETRY_USER_TEXT,
|
|
20
|
-
MISSED_TOOL_FAILURE_BUBBLE,
|
|
21
|
-
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
22
|
-
NEW_CONVERSATION_TOOL_NAME,
|
|
23
13
|
parseContactToolCalls,
|
|
24
|
-
READ_SESSION_TOOL_NAME,
|
|
25
|
-
SCHEDULE_TASK_TOOL_NAME,
|
|
26
|
-
STOP_SESSION_TOOL_NAME,
|
|
27
|
-
STEER_SESSION_TOOL_NAME,
|
|
28
|
-
ARCHIVE_SESSION_TOOL_NAME,
|
|
29
|
-
DELETE_SESSION_TOOL_NAME,
|
|
30
14
|
stripContactToolFences,
|
|
31
|
-
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
32
|
-
WATCH_SESSION_TOOL_NAME,
|
|
33
15
|
} from './contact-tools.js';
|
|
34
16
|
import {
|
|
35
17
|
createPiCodingRuntime,
|
|
@@ -121,7 +103,9 @@ export const CONTACT_SYSTEM_PROMPT = [
|
|
|
121
103
|
'Reply in short chat bubbles: a few sentences each, separated by a blank line.',
|
|
122
104
|
'Talk like a person in the user\'s language. One short spoken bubble at a time — never a wall of paragraphs.',
|
|
123
105
|
'Always reply in {{LANGUAGE}} — the user\'s current interface language — even when the user writes in another language, unless they explicitly ask for a different one.',
|
|
124
|
-
'
|
|
106
|
+
'Keep private reasoning and tool traces private. Communicate your next concrete action, verified milestones, and blockers in short natural messages in {{LANGUAGE}}.',
|
|
107
|
+
'Before starting work, send a short message explaining the next action. During multi-step work, send a message when a meaningful result arrives or the direction changes. Publish these messages during execution; keep the final answer to the remaining outcome instead of repeating earlier updates.',
|
|
108
|
+
'Public message protocol: put each user-facing progress message at the START of your response in its own openchamber-message JSON fence, with a single-line JSON object {"text":"Your short message"}. Then emit the next tool call or final answer. A closed message fence is delivered immediately while you are still generating. Include only text intended for the user. Tool results are evidence for you to summarize in your own words; never copy their stock confirmation wording into your reply.',
|
|
125
109
|
'Do not expose tool traces, Activity, or editor actions.',
|
|
126
110
|
'You have bash, read, write, and edit in the working directory. Use them for pwd, files, and shell. Never say you have no terminal or cannot read files. Ignore any temporary generator workspace in the environment.',
|
|
127
111
|
'Understand natural language in any language, including Chinese: 开新对话 / 清除记忆 means new_conversation (LLM memory only, chat history stays), 清空聊天记录 means clear_chat_history (delete transcript), 找项目 means list_projects, 现有对话 means list_sessions, 查看助手设定 / 默认提示词 means get_assistant_settings (pass to="Name" for another assistant), 改默认提示词 / 设置人设 / 改某助手的默认提示词 means update_default_prompt (persists that assistant\'s settings, later turns only; pass to="OpenCode 配置助手" to edit another contact without changing this one), 建助理 means create_assistant, 建会话 / 开个新会话 / 继续会话 means assign_session, 监听会话 means watch_session, 停止/取消/打断会话 means stop_session, 插话 means steer_session, 归档会话 means archive_session, 删除会话 means delete_session, 排定时任务 means schedule_task, 给 X 说一声 means message_assistant, 发卡片 means emit a card via those tools — never ask the user to type /card or /dm.',
|
|
@@ -193,10 +177,9 @@ const assistantMessage = (model, text, stopReason, errorMessage) => ({
|
|
|
193
177
|
/**
|
|
194
178
|
* streamFn for pi-agent-core. Calls OpenChamber completions (public HTTP stays
|
|
195
179
|
* non-streaming). In-process callers may pass onTextDelta/globalEventHub for
|
|
196
|
-
* the throwaway generate path.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* confirm. Must not throw — encode failures on the event stream.
|
|
180
|
+
* the throwaway generate path. Complete public prefix records publish during
|
|
181
|
+
* generation; ordinary raw tokens stay private. Legacy prose and final replies
|
|
182
|
+
* publish after parsing. Failures are encoded on the event stream.
|
|
200
183
|
*/
|
|
201
184
|
const completionFileParts = (value) => (Array.isArray(value) ? value : [])
|
|
202
185
|
.filter((part) => part?.type === 'file' && typeof part.mime === 'string' && typeof part.url === 'string')
|
|
@@ -210,32 +193,54 @@ const completionFileParts = (value) => (Array.isArray(value) ? value : [])
|
|
|
210
193
|
/** Fence start (openchamber-tool or generic ```) — stop live bubble deltas. */
|
|
211
194
|
const CONTACT_FENCE_START = /```/;
|
|
212
195
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
196
|
+
// Only explicitly public, complete prefix records may publish before completion.
|
|
197
|
+
// Retain at most one bounded incomplete record; ordinary text and tool JSON stay private.
|
|
198
|
+
function createPublicMessageReader(publish) {
|
|
199
|
+
const marker = '```openchamber-message';
|
|
200
|
+
let pending = '';
|
|
201
|
+
let stopped = false;
|
|
202
|
+
return {
|
|
203
|
+
push(delta) {
|
|
204
|
+
if (stopped || typeof delta !== 'string') return;
|
|
205
|
+
pending += delta;
|
|
206
|
+
while (true) {
|
|
207
|
+
pending = pending.trimStart();
|
|
208
|
+
if (!pending || marker.startsWith(pending)) return;
|
|
209
|
+
if (!pending.startsWith(marker)) { stopped = true; return; }
|
|
210
|
+
const match = pending.match(/^```openchamber-message[ \t]*\r?\n([^\n]+)\r?\n```/u);
|
|
211
|
+
if (match && match[0].length > 8192) { stopped = true; return; }
|
|
212
|
+
if (!match) {
|
|
213
|
+
if (pending.length > 8192) stopped = true;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let value;
|
|
217
|
+
try {
|
|
218
|
+
value = JSON.parse(match[1]);
|
|
219
|
+
if (typeof value?.text !== 'string' || !value.text.trim() || value.text.length > 2000) {
|
|
220
|
+
stopped = true;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
} catch { stopped = true; return; }
|
|
224
|
+
publish(value.text.trim());
|
|
225
|
+
pending = pending.slice(match[0].length);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
234
230
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
]);
|
|
231
|
+
function stripPublicMessages(text) {
|
|
232
|
+
let rest = text.trimStart();
|
|
233
|
+
while (rest.startsWith('```openchamber-message')) {
|
|
234
|
+
const match = rest.match(/^```openchamber-message[ \t]*\r?\n([^\n]+)\r?\n```/u);
|
|
235
|
+
if (!match || match[0].length > 8192) throw new Error('Invalid public message record');
|
|
236
|
+
const value = JSON.parse(match[1]);
|
|
237
|
+
if (typeof value?.text !== 'string' || !value.text.trim() || value.text.length > 2000) {
|
|
238
|
+
throw new Error('Invalid public message text');
|
|
239
|
+
}
|
|
240
|
+
rest = rest.slice(match[0].length).trimStart();
|
|
241
|
+
}
|
|
242
|
+
return rest;
|
|
243
|
+
}
|
|
239
244
|
|
|
240
245
|
const assistantTextParts = (message) => (Array.isArray(message?.content) ? message.content : [])
|
|
241
246
|
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
@@ -257,7 +262,7 @@ export function projectStreamedContactTurnBubbles(messages, turnStart = 0) {
|
|
|
257
262
|
const hasTool = parts.some((part) => part?.type === 'toolCall');
|
|
258
263
|
const text = assistantTextParts(message).trim();
|
|
259
264
|
if (!text) continue;
|
|
260
|
-
if (hasTool && !isContactSpokenPreamble(text)) continue;
|
|
265
|
+
if (hasTool && message.contactPublicText !== true && !isContactSpokenPreamble(text)) continue;
|
|
261
266
|
for (const bubble of splitContactBubbles(text)) bubbles.push(bubble);
|
|
262
267
|
}
|
|
263
268
|
return bubbles;
|
|
@@ -360,11 +365,22 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
360
365
|
signal = null,
|
|
361
366
|
/** Fixed Agent message length before this contact turn (prior history only). */
|
|
362
367
|
turnMessageStart = 0,
|
|
368
|
+
variant = null,
|
|
363
369
|
} = {}) {
|
|
364
370
|
let callSequence = 0;
|
|
365
371
|
const turnStart = Number.isFinite(Number(turnMessageStart)) ? Math.max(0, Number(turnMessageStart)) : 0;
|
|
366
372
|
return (model, context) => {
|
|
367
373
|
const stream = createAssistantMessageEventStream();
|
|
374
|
+
const publicBubbles = [];
|
|
375
|
+
const baseIndex = projectStreamedContactTurnBubbles(context.messages, turnStart).length;
|
|
376
|
+
const publishPublicMessage = (text) => {
|
|
377
|
+
signal?.throwIfAborted();
|
|
378
|
+
for (const bubble of splitContactBubbles(text)) {
|
|
379
|
+
const index = baseIndex + publicBubbles.length;
|
|
380
|
+
publicBubbles.push(bubble);
|
|
381
|
+
onBubbleDelta?.(index, bubble, true);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
368
384
|
const run = async () => {
|
|
369
385
|
try {
|
|
370
386
|
const fallbackFiles = completionFileParts(pendingFileParts);
|
|
@@ -427,6 +443,8 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
427
443
|
// Real results stay in messages, so already completed work is not replayed.
|
|
428
444
|
for (let attempt = 0; ; attempt += 1) {
|
|
429
445
|
signal?.throwIfAborted();
|
|
446
|
+
const publicStart = publicBubbles.length;
|
|
447
|
+
const publicReader = createPublicMessageReader(publishPublicMessage);
|
|
430
448
|
const result = await createChatCompletion({
|
|
431
449
|
signal,
|
|
432
450
|
body: {
|
|
@@ -435,15 +453,28 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
435
453
|
? model.name.split('/')[0]
|
|
436
454
|
: undefined,
|
|
437
455
|
modelID: model.id,
|
|
456
|
+
...(variant ? { variant } : {}),
|
|
438
457
|
messages: [...messages],
|
|
439
458
|
},
|
|
440
459
|
onTextDelta: (delta) => {
|
|
460
|
+
if (signal?.aborted) return;
|
|
461
|
+
if (typeof delta === 'string') {
|
|
462
|
+
publicReader.push(delta);
|
|
463
|
+
}
|
|
441
464
|
if (typeof delta === 'string' && delta && typeof onTextDelta === 'function') onTextDelta(delta);
|
|
442
465
|
},
|
|
443
466
|
globalEventHub,
|
|
444
467
|
});
|
|
445
468
|
signal?.throwIfAborted();
|
|
446
469
|
text = result?.completion?.choices?.[0]?.message?.content ?? result?.text ?? '';
|
|
470
|
+
const finalPublic = [];
|
|
471
|
+
createPublicMessageReader((value) => finalPublic.push(...splitContactBubbles(value))).push(text);
|
|
472
|
+
const observedPublic = publicBubbles.slice(publicStart);
|
|
473
|
+
if (observedPublic.some((value, index) => finalPublic[index] !== value)) {
|
|
474
|
+
throw new Error('Model changed an already published public message');
|
|
475
|
+
}
|
|
476
|
+
for (const value of finalPublic.slice(observedPublic.length)) publishPublicMessage(value);
|
|
477
|
+
text = stripPublicMessages(text);
|
|
447
478
|
const finalResponse = parseFinalResponse(text);
|
|
448
479
|
finalText = finalResponse?.text ?? null;
|
|
449
480
|
// A final answer is opaque text; quoted tool examples in it cannot run.
|
|
@@ -462,14 +493,17 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
462
493
|
if (attempt >= 2) throw new Error('Assistant response protocol failed after two corrections; the task was not confirmed complete.');
|
|
463
494
|
const rejected = parsed.toolCalls.filter((call) => isPiCodingToolName(call.name));
|
|
464
495
|
if (rejected.length > 0) rejectedCodingCalls = rejected.map(({ name, arguments: args }) => ({ name, arguments: args }));
|
|
496
|
+
if (publicBubbles.length > publicStart) {
|
|
497
|
+
messages.push({ role: 'assistant', content: publicBubbles.slice(publicStart).join('\n\n') });
|
|
498
|
+
}
|
|
465
499
|
// Never replay rejected prose: it may contain fabricated user/tool results.
|
|
466
500
|
messages.push(
|
|
467
501
|
{ role: 'user', content: `OpenChamber response protocol correction: ${protocolError} No tool calls from the rejected response executed. ${rejectedCodingCalls.length ? `Unexecuted proposed calls (not results): ${JSON.stringify(rejectedCodingCalls)}.` : ''} Continue the remaining work using the supplied real results; do not repeat completed operations. ${requiresFinal ? WORKSPACE_RESPONSE_PROTOCOL : 'Emit the corrected openchamber-tool call.'}` },
|
|
468
502
|
);
|
|
469
503
|
}
|
|
470
504
|
// Continue turn-global bubble indices from already-streamed turn messages.
|
|
471
|
-
const
|
|
472
|
-
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta, { baseIndex });
|
|
505
|
+
const nextIndex = baseIndex + publicBubbles.length;
|
|
506
|
+
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta, { baseIndex: nextIndex });
|
|
473
507
|
if (parsed.toolCalls.length > 0) {
|
|
474
508
|
const toolCalls = parsed.toolCalls.map((call) => ({
|
|
475
509
|
type: 'toolCall',
|
|
@@ -479,22 +513,24 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
479
513
|
}));
|
|
480
514
|
const spoken = isContactSpokenPreamble(parsed.chatText) ? parsed.chatText.trim() : '';
|
|
481
515
|
const spokenBubbles = spoken ? splitContactBubbles(spoken) : [];
|
|
482
|
-
const
|
|
483
|
-
|
|
516
|
+
const publicText = [...publicBubbles, ...spokenBubbles].join('\n\n');
|
|
517
|
+
const content = publicText
|
|
518
|
+
? [{ type: 'text', text: publicText }, ...toolCalls]
|
|
484
519
|
: toolCalls;
|
|
485
520
|
const partial = {
|
|
486
521
|
...assistantMessage(model, spoken, 'toolUse'),
|
|
487
522
|
content,
|
|
523
|
+
contactPublicText: true,
|
|
488
524
|
};
|
|
489
525
|
if (spokenBubbles.length > 0) bubbleTracker.finish(spokenBubbles);
|
|
490
526
|
stream.push({ type: 'start', partial });
|
|
491
|
-
if (
|
|
527
|
+
if (publicText) {
|
|
492
528
|
stream.push({ type: 'text_start', contentIndex: 0, partial });
|
|
493
|
-
stream.push({ type: 'text_delta', contentIndex: 0, delta:
|
|
494
|
-
stream.push({ type: 'text_end', contentIndex: 0, content:
|
|
529
|
+
stream.push({ type: 'text_delta', contentIndex: 0, delta: publicText, partial });
|
|
530
|
+
stream.push({ type: 'text_end', contentIndex: 0, content: publicText, partial });
|
|
495
531
|
}
|
|
496
532
|
toolCalls.forEach((toolCall, index) => {
|
|
497
|
-
const toolIndex = index + (
|
|
533
|
+
const toolIndex = index + (publicText ? 1 : 0);
|
|
498
534
|
stream.push({ type: 'toolcall_start', contentIndex: toolIndex, partial });
|
|
499
535
|
stream.push({ type: 'toolcall_delta', contentIndex: toolIndex, delta: JSON.stringify(toolCall.arguments), partial });
|
|
500
536
|
stream.push({ type: 'toolcall_end', contentIndex: toolIndex, toolCall, partial });
|
|
@@ -507,7 +543,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
507
543
|
const replyBubbles = splitContactBubbles(chatText);
|
|
508
544
|
if (bubbleGapMs > 0 && replyBubbles.length > 1) {
|
|
509
545
|
for (let index = 0; index < replyBubbles.length; index += 1) {
|
|
510
|
-
onBubbleDelta?.(
|
|
546
|
+
onBubbleDelta?.(nextIndex + index, replyBubbles[index], true);
|
|
511
547
|
if (index < replyBubbles.length - 1) {
|
|
512
548
|
await new Promise((resolve) => setTimeout(resolve, bubbleGapMs));
|
|
513
549
|
}
|
|
@@ -515,7 +551,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
515
551
|
} else {
|
|
516
552
|
bubbleTracker.finish(replyBubbles);
|
|
517
553
|
}
|
|
518
|
-
const partial = assistantMessage(model,
|
|
554
|
+
const partial = assistantMessage(model, [...publicBubbles, ...replyBubbles].join('\n\n'), 'stop');
|
|
519
555
|
stream.push({ type: 'start', partial });
|
|
520
556
|
stream.push({ type: 'text_start', contentIndex: 0, partial });
|
|
521
557
|
stream.push({ type: 'text_delta', contentIndex: 0, delta: chatText, partial });
|
|
@@ -555,25 +591,11 @@ const extractAssistantText = (messages) => {
|
|
|
555
591
|
return '';
|
|
556
592
|
};
|
|
557
593
|
|
|
558
|
-
const extractToolResultText = (messages) => {
|
|
559
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
560
|
-
const message = messages[index];
|
|
561
|
-
if (message?.role !== 'toolResult') continue;
|
|
562
|
-
const text = (message.content || [])
|
|
563
|
-
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
564
|
-
.map((part) => part.text)
|
|
565
|
-
.join('')
|
|
566
|
-
.trim();
|
|
567
|
-
if (text) return text;
|
|
568
|
-
}
|
|
569
|
-
return '';
|
|
570
|
-
};
|
|
571
|
-
|
|
572
594
|
/**
|
|
573
595
|
* Final turn bubbles reuse the same projection as live onBubbleDelta indices
|
|
574
596
|
* (every spoken preamble + final assistant text; independent messages keep
|
|
575
|
-
* duplicate text). Tool
|
|
576
|
-
*
|
|
597
|
+
* duplicate text). Tool results remain model input. Reset uses a fresh model
|
|
598
|
+
* confirmation with the completed reset result as its only task context.
|
|
577
599
|
*/
|
|
578
600
|
const extractContactTurnOutcome = (messages) => {
|
|
579
601
|
const slice = Array.isArray(messages) ? messages : [];
|
|
@@ -581,38 +603,8 @@ const extractContactTurnOutcome = (messages) => {
|
|
|
581
603
|
const hasTool = contactTurnHasToolResult(slice);
|
|
582
604
|
// Same rules as live SSE indices — keep every independent spoken/final bubble.
|
|
583
605
|
const published = projectStreamedContactTurnBubbles(slice, 0);
|
|
584
|
-
|
|
585
|
-
const coding = isPiCodingToolName(lastToolName);
|
|
586
|
-
const toolText = extractToolResultText(slice);
|
|
587
|
-
const assistant = extractAssistantText(slice);
|
|
588
|
-
// Tool-result text is never projected mid-turn; append only when it is the
|
|
589
|
-
// user-facing confirm (confirm-only / list tools). Spoken + assistant text
|
|
590
|
-
// already live in `published`. Card/side-effect and read_session never paint
|
|
591
|
-
// English tool payloads.
|
|
592
|
-
let toolOnlyConfirm = '';
|
|
593
|
-
if (hasTool && !coding) {
|
|
594
|
-
if (TOOL_TEXT_BUBBLE_TOOLS.has(lastToolName)) {
|
|
595
|
-
toolOnlyConfirm = toolText || '';
|
|
596
|
-
if (!toolOnlyConfirm && published.length === 0) toolOnlyConfirm = assistant || '';
|
|
597
|
-
} else if (CARD_SIDE_EFFECT_TOOLS.has(lastToolName) || MODEL_ONLY_LOOKUP_TOOLS.has(lastToolName)) {
|
|
598
|
-
toolOnlyConfirm = '';
|
|
599
|
-
} else {
|
|
600
|
-
// list_projects / list_sessions and other app tools keep toolText.
|
|
601
|
-
toolOnlyConfirm = toolText || '';
|
|
602
|
-
if (!toolOnlyConfirm && published.length === 0) toolOnlyConfirm = assistant || '';
|
|
603
|
-
}
|
|
604
|
-
} else if (hasTool && coding && published.length === 0) {
|
|
605
|
-
toolOnlyConfirm = toolText || '';
|
|
606
|
-
}
|
|
606
|
+
extractAssistantText(slice); // Preserve model errors even after earlier public messages.
|
|
607
607
|
const bubbles = [...published];
|
|
608
|
-
if (toolOnlyConfirm) {
|
|
609
|
-
const confirmBubbles = splitContactBubbles(toolOnlyConfirm);
|
|
610
|
-
const trailing = bubbles.slice(-confirmBubbles.length);
|
|
611
|
-
const alreadySuffix = confirmBubbles.length > 0
|
|
612
|
-
&& trailing.length === confirmBubbles.length
|
|
613
|
-
&& trailing.every((item, index) => item === confirmBubbles[index]);
|
|
614
|
-
if (!alreadySuffix) bubbles.push(...confirmBubbles);
|
|
615
|
-
}
|
|
616
608
|
return {
|
|
617
609
|
text: bubbles.join('\n\n'),
|
|
618
610
|
bubbles,
|
|
@@ -711,6 +703,7 @@ export async function runContactTurn({
|
|
|
711
703
|
messages: prior,
|
|
712
704
|
},
|
|
713
705
|
streamFn: createContactStreamFn(createChatCompletion, {
|
|
706
|
+
variant: assistant.variant,
|
|
714
707
|
pendingFileParts,
|
|
715
708
|
onTextDelta,
|
|
716
709
|
onBubbleDelta,
|
|
@@ -747,8 +740,8 @@ export async function runContactTurn({
|
|
|
747
740
|
}
|
|
748
741
|
}
|
|
749
742
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
750
|
-
//
|
|
751
|
-
//
|
|
743
|
+
// Preserve model-authored messages across the retry. An empty outcome is
|
|
744
|
+
// a turn error; the harness never manufactures an assistant reply.
|
|
752
745
|
const missed = extractContactTurnOutcome(agent.state.messages.slice(turnMessageStart));
|
|
753
746
|
const missedBubbles = (Array.isArray(missed.bubbles) ? missed.bubbles : [])
|
|
754
747
|
.map((item) => (typeof item === 'string' ? stripContactToolFences(item).trim() : ''))
|
|
@@ -762,35 +755,49 @@ export async function runContactTurn({
|
|
|
762
755
|
tools: [...agent.state.tools],
|
|
763
756
|
};
|
|
764
757
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
758
|
+
throw Object.assign(new Error('Assistant did not perform the requested operation'), { code: 'upstream_error' });
|
|
759
|
+
}
|
|
760
|
+
const turnMessages = agent.state.messages.slice(turnMessageStart);
|
|
761
|
+
const reset = contactTurnHasSuccessfulReset(turnMessages);
|
|
762
|
+
const lastMessage = turnMessages.at(-1);
|
|
763
|
+
// Terminal tools stop further mutations. A tools-disabled model pass owns
|
|
764
|
+
// their user-facing result, including failures and successful resets.
|
|
765
|
+
if (reset || lastMessage?.role === 'toolResult') {
|
|
766
|
+
const summaryMessages = reset
|
|
767
|
+
? [turnMessages.findLast((message) => contactTurnHasSuccessfulReset([message]))]
|
|
768
|
+
: [...turnMessages];
|
|
769
|
+
const summaryStream = createContactStreamFn(createChatCompletion, {
|
|
770
|
+
variant: assistant.variant,
|
|
771
|
+
signal,
|
|
772
|
+
globalEventHub,
|
|
773
|
+
onBubbleDelta: reset ? null : onBubbleDelta,
|
|
774
|
+
})(model, {
|
|
775
|
+
systemPrompt: `Reply briefly in ${resolveContactLanguage(language)}. Summarize the supplied actual operation result in your own natural words. Tool result text is internal evidence. Keep private diagnostics and stock confirmation wording out of your reply. Report a failure honestly. The operation has already run; this pass only communicates its result.`,
|
|
776
|
+
messages: summaryMessages,
|
|
777
|
+
tools: [],
|
|
778
|
+
});
|
|
779
|
+
const summary = await summaryStream.result();
|
|
780
|
+
signal?.throwIfAborted();
|
|
781
|
+
if (summary.stopReason === 'error' || summary.stopReason === 'aborted') {
|
|
782
|
+
throw Object.assign(new Error(summary.errorMessage || 'Assistant result summary failed'), { code: 'upstream_error' });
|
|
783
|
+
}
|
|
784
|
+
if (!assistantTextParts(summary).trim()) {
|
|
785
|
+
throw Object.assign(new Error('Assistant returned no result summary'), { code: 'upstream_error' });
|
|
786
|
+
}
|
|
787
|
+
if (reset) {
|
|
788
|
+
const resetBubbles = [assistantTextParts(summary).trim()];
|
|
789
|
+
return {
|
|
790
|
+
text: resetBubbles.join('\n\n'), bubbles: resetBubbles, cards: [], reset: true,
|
|
791
|
+
historyCleared: contactTurnClearedChatHistory(turnMessages),
|
|
792
|
+
thinkingLevel: agent.state.thinkingLevel, tools: [...agent.state.tools],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
turnMessages.push(summary);
|
|
772
796
|
}
|
|
773
797
|
// Final bubbles share the turn-boundary projection with live onBubbleDelta indices.
|
|
774
|
-
const outcome = extractContactTurnOutcome(
|
|
798
|
+
const outcome = extractContactTurnOutcome(turnMessages);
|
|
775
799
|
const bubbles = Array.isArray(outcome.bubbles) ? outcome.bubbles : [];
|
|
776
800
|
const text = bubbles.join('\n\n') || outcome.text || '';
|
|
777
|
-
if (contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
778
|
-
const historyCleared = contactTurnClearedChatHistory(agent.state.messages);
|
|
779
|
-
const preferredConfirm = historyCleared
|
|
780
|
-
? CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE
|
|
781
|
-
: NEW_CONVERSATION_CONFIRM_BUBBLE;
|
|
782
|
-
// Reset is independent: discard mid-turn spokens; keep only the unique confirm.
|
|
783
|
-
const resetBubbles = confirmBubbleAfterContactReset(bubbles, preferredConfirm);
|
|
784
|
-
return {
|
|
785
|
-
text: resetBubbles[0] || preferredConfirm,
|
|
786
|
-
bubbles: resetBubbles,
|
|
787
|
-
cards: [],
|
|
788
|
-
reset: true,
|
|
789
|
-
historyCleared,
|
|
790
|
-
thinkingLevel: agent.state.thinkingLevel,
|
|
791
|
-
tools: [...agent.state.tools],
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
801
|
// Card tools / side-effect tools may finish with cards only (no English toolText bubble).
|
|
795
802
|
if (!text.trim() && outcome.cards.length === 0 && !outcome.hasTool) {
|
|
796
803
|
const error = new Error('Assistant returned no text');
|