@openchambery/web 1.19.14 → 1.19.15-beta.1
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-Dncbu_Z2.js → AssistantView-D5rEZ-Ss.js} +2 -2
- package/dist/assets/{DiagramView-CovEl1BY.js → DiagramView-DaB9Sb7S.js} +1 -1
- package/dist/assets/MarkdownRenderer-emLeoNjI.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-CCfHY-86.js → MarkdownRendererImpl-BWCAPlqn.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-y6QgIUZA.js → MarkstreamRendererImpl-BD_NFKUv.js} +1 -1
- package/dist/assets/MobileDetailNavigation-DqXVAIap.js +1 -0
- package/dist/assets/{MobileShareBridge-B4S7BaUj.js → MobileShareBridge-Bk2fdkJk.js} +1 -1
- package/dist/assets/MobileSurface-C4mpcOCh.js +2 -0
- package/dist/assets/{MultiRunWindow-BeHa-CG6.js → MultiRunWindow-rs49_K1E.js} +1 -1
- package/dist/assets/{SettingsView-Dn7odtQz.js → SettingsView-b8LUjoad.js} +2 -2
- package/dist/assets/{SettingsWindow-C0tWPS5f.js → SettingsWindow-D8JFlPdA.js} +1 -1
- package/dist/assets/{TerminalView-BnJgC7Qo.js → TerminalView-DCd-Godi.js} +1 -1
- package/dist/assets/{ToolOutputDialog-D9S-X8hM.js → ToolOutputDialog-B50L_nUP.js} +1 -1
- package/dist/assets/{appThemeRegistry-DBrvaJnq.js → appThemeRegistry-BWtCaTMe.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-Dc004xOh.js → fileMentionTouchSelection-DKYkoypI.js} +1 -1
- package/dist/assets/{gitApi-Cm3JLQqh.js → gitApi-D4PyYgLZ.js} +1 -1
- package/dist/assets/{insertionBoundaries-CTWYFsbH.js → insertionBoundaries-Bnnx03Wj.js} +1 -1
- package/dist/assets/{main-CSqu3MB4.js → main-BRiQTNFX.js} +4 -4
- package/dist/assets/{main-DwMprEyr.js → main-CnY0LWNc.js} +2 -2
- package/dist/assets/{miniChat-C8bzY_Ye.js → miniChat-B0pw_mUs.js} +2 -2
- package/dist/assets/{mobile-Oy-SRKkr.js → mobile-BKHmxwXz.js} +2 -2
- package/dist/assets/{multirun-ClcuaiKU.js → multirun-BQKqVfFT.js} +1 -1
- package/dist/assets/{projectMeta-F93wE2Ye.js → projectMeta-BvG8sdtC.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-rOOhMVlQ.js → renderElectronMiniChatApp-E5a0yy7E.js} +2 -2
- package/dist/assets/{renderMobileApp-BP_hGwHP.js → renderMobileApp-DT0L4Ah6.js} +4 -4
- package/dist/assets/{runtimeConfig-iJ2mTBVN.js → runtimeConfig-utWzrDwa.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ISnhAS3q.js → runtimeEndpointReset-DE6lwHyg.js} +1 -1
- package/dist/assets/sessionChangeSummary-BG7l2heP.js +2 -0
- package/dist/assets/{sessionLookup-DQQ9-DuP.js → sessionLookup-12zVOR0J.js} +1 -1
- package/dist/assets/src-CFUi4riW.css +2 -0
- package/dist/assets/{useAppFontEffects--IcU7jyv.js → useAppFontEffects-BhYuP4L2.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-BqJ5R_T2.js → useDesktopWindowControlsLayout-DPNMRkrI.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-ChWXV8xM.js → useEffectiveDirectory-Ji8AKgzY.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DQkgxuzg.js → useMobileNavigationStore-BYdpuwL7.js} +1 -1
- package/dist/assets/{useSessionAutoCleanup-DWWAvQQ0.js → useSessionAutoCleanup-CADZCvZL.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-D2oQszsk.js → useWorkerHighlightedLines-CuG88Lsj.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +33 -12
- package/server/lib/assistants/contact-store.js +25 -1
- package/server/lib/assistants/contact-store.test.js +39 -5
- package/server/lib/assistants/contact-tools.js +3 -3
- package/server/lib/assistants/harness.js +136 -101
- package/server/lib/assistants/harness.test.js +235 -8
- package/server/lib/assistants/pi-tools.js +2 -0
- package/server/lib/assistants/pi-tools.test.js +8 -0
- package/server/lib/assistants/service.js +313 -74
- package/server/lib/assistants/service.test.js +555 -18
- package/server/lib/llm/DOCUMENTATION.md +9 -4
- package/server/lib/llm/generate.js +45 -6
- package/server/lib/llm/generate.test.js +52 -0
- package/dist/assets/MarkdownRenderer-CZqok-Cf.js +0 -2
- package/dist/assets/MobileDetailNavigation-Ca-d5eZ9.js +0 -1
- package/dist/assets/MobileSurface-xL5Jby4U.js +0 -2
- package/dist/assets/sessionChangeSummary-BAzU2J1H.js +0 -2
- package/dist/assets/src-DYeppLfP.css +0 -2
|
@@ -115,7 +115,9 @@ export const resolveContactLanguage = (value) => {
|
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
export const CONTACT_SYSTEM_PROMPT = [
|
|
118
|
-
"You are OpenChamber's in-app assistant — a personable contact
|
|
118
|
+
"You are OpenChamber's in-app assistant — a personable contact focused on understanding the user, communicating naturally, coordinating work, and following up on results.",
|
|
119
|
+
'For substantive tasks, default to finding the relevant existing OpenCode workspace and handing the task to a worker session with assign_session. Carry the user goal, relevant context, constraints, and verification expectations into that session; follow its results and explain the outcome in normal conversation.',
|
|
120
|
+
'Handle simple questions, file lookups, and small, clearly scoped configuration changes directly when their scope and verification are straightforward. As a task grows into implementation, multi-step debugging, or broad changes, hand it to a worker session. An explicit user request to handle a bounded task yourself can use the direct tools.',
|
|
119
121
|
'Reply in short chat bubbles: a few sentences each, separated by a blank line.',
|
|
120
122
|
'Talk like a person in the user\'s language. One short spoken bubble at a time — never a wall of paragraphs.',
|
|
121
123
|
'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.',
|
|
@@ -123,17 +125,18 @@ export const CONTACT_SYSTEM_PROMPT = [
|
|
|
123
125
|
'Do not expose tool traces, Activity, or editor actions.',
|
|
124
126
|
'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.',
|
|
125
127
|
'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.',
|
|
126
|
-
'You receive the registered project catalog every turn.
|
|
127
|
-
'
|
|
128
|
+
'You receive the registered project catalog every turn. Match the user goal against project labels and paths and existing OpenCode conversations via list_sessions. Start in the user-configured working directory; managed contacts start in the current server user home directory and can inspect user-global directories with read/bash. When the starting directory has no relevant session, walk its parent directories to find the nearest relevant existing OpenCode workspace using session directory metadata, then search that workspace for the requested project or conversation. Use an unscoped list_sessions lookup when broader context is needed. Prefer the user-configured directory and explicit session context; ask a short question when several relevant targets remain. Keep discovery bounded and use actual tool results as evidence.',
|
|
129
|
+
'An empty registered catalog still permits looking up existing sessions and inspecting the working directory. Resolve the existing workspace first. Assignment requires a registered project: when the resolved workspace needs registration, explain that specific prerequisite and ask the user to register it in Settings. Workspace discovery should preserve the user existing directory structure.',
|
|
130
|
+
'For substantive work, match the workspace, inspect relevant existing chats with list_sessions, then create a worker through assign_session with projectPath or continue the relevant conversation with sessionID plus the task prompt (omit model args to keep the prior worker model). File and shell tools support discovery and the small-task exception. To only listen without prompting when asked (监听/watch/monitor), watch_session with sessionID — a plain @session:id mention alone is not a watch request. To abort a running session, stop_session with sessionID. Optional worker model via providerID/modelID/model from the connected catalog — that does not change this contact. Current-turn user attachments are server-forwarded on assign. One successful assign_session or watch_session ends the turn — do not call it again.',
|
|
128
131
|
'A reply without the tool call does nothing. Never say 已创建, created, scheduled, opened, watched, or stopped unless the tool already returned success.',
|
|
129
132
|
].join(' ');
|
|
130
133
|
|
|
131
134
|
// A workspace response must explicitly finish or ask for missing input. Plain
|
|
132
135
|
// progress prose is not a final answer and must not silently end the pi loop.
|
|
133
136
|
const WORKSPACE_RESPONSE_PROTOCOL = [
|
|
134
|
-
'Workspace response protocol: continue using openchamber-tool calls until the
|
|
137
|
+
'Workspace response protocol: continue using openchamber-tool calls until the task is handed to a worker session, or the small direct task and its verification are done. A successful assign_session ends this turn and worker completion is reported through the session follow-up.',
|
|
135
138
|
'You may batch independent coding calls, but they execute sequentially in the order supplied. Wait for results before using their output. Send OpenChamber operation tools one at a time.',
|
|
136
|
-
'Reading a skill
|
|
139
|
+
'Reading a skill loads instructions. For substantive work, pass relevant instructions and context to the worker session; for a small direct task, execute the applicable steps and verify the result before completing.',
|
|
137
140
|
'To finish, reply with only an openchamber-final JSON fence: ```openchamber-final\n{"status":"complete","text":"Your final answer in the user language"}\n```.',
|
|
138
141
|
'Use status "blocked" with the missing information or real blocker when you cannot proceed. Questions and ordinary conversation also use this final format; they do not require a tool call.',
|
|
139
142
|
'Do not use a final declaration for a progress update or promise to act later. Never invent tool results, User messages, or future assistant turns. Only supplied tool-result records are execution evidence.',
|
|
@@ -207,14 +210,69 @@ const completionFileParts = (value) => (Array.isArray(value) ? value : [])
|
|
|
207
210
|
/** Fence start (openchamber-tool or generic ```) — stop live bubble deltas. */
|
|
208
211
|
const CONTACT_FENCE_START = /```/;
|
|
209
212
|
|
|
213
|
+
/** Tool confirms that are themselves the user-facing bubble (no card). */
|
|
214
|
+
const TOOL_TEXT_BUBBLE_TOOLS = new Set([
|
|
215
|
+
STOP_SESSION_TOOL_NAME,
|
|
216
|
+
STEER_SESSION_TOOL_NAME,
|
|
217
|
+
ARCHIVE_SESSION_TOOL_NAME,
|
|
218
|
+
DELETE_SESSION_TOOL_NAME,
|
|
219
|
+
NEW_CONVERSATION_TOOL_NAME,
|
|
220
|
+
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
221
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
222
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
/** Card / side-effect tools: never paint English toolText into the transcript. */
|
|
226
|
+
const CARD_SIDE_EFFECT_TOOLS = new Set([
|
|
227
|
+
ASSIGN_SESSION_TOOL_NAME,
|
|
228
|
+
WATCH_SESSION_TOOL_NAME,
|
|
229
|
+
STOP_SESSION_TOOL_NAME,
|
|
230
|
+
CREATE_ASSISTANT_TOOL_NAME,
|
|
231
|
+
SCHEDULE_TASK_TOOL_NAME,
|
|
232
|
+
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
/** Lookup payloads for the model only — never paint quoted JSON as a user bubble. */
|
|
236
|
+
const MODEL_ONLY_LOOKUP_TOOLS = new Set([
|
|
237
|
+
READ_SESSION_TOOL_NAME,
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const assistantTextParts = (message) => (Array.isArray(message?.content) ? message.content : [])
|
|
241
|
+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
242
|
+
.map((part) => part.text)
|
|
243
|
+
.join('');
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Streamed assistant bubbles already present in this turn's Agent messages.
|
|
247
|
+
* Spoken preambles (tool turns) and final assistant text share splitContactBubbles.
|
|
248
|
+
* Independent messages keep duplicate text. Tool-result confirms are not streamed here.
|
|
249
|
+
*/
|
|
250
|
+
export function projectStreamedContactTurnBubbles(messages, turnStart = 0) {
|
|
251
|
+
const start = Number.isFinite(Number(turnStart)) ? Math.max(0, Number(turnStart)) : 0;
|
|
252
|
+
const list = Array.isArray(messages) ? messages.slice(start) : [];
|
|
253
|
+
const bubbles = [];
|
|
254
|
+
for (const message of list) {
|
|
255
|
+
if (message?.role !== 'assistant') continue;
|
|
256
|
+
const parts = Array.isArray(message.content) ? message.content : [];
|
|
257
|
+
const hasTool = parts.some((part) => part?.type === 'toolCall');
|
|
258
|
+
const text = assistantTextParts(message).trim();
|
|
259
|
+
if (!text) continue;
|
|
260
|
+
if (hasTool && !isContactSpokenPreamble(text)) continue;
|
|
261
|
+
for (const bubble of splitContactBubbles(text)) bubbles.push(bubble);
|
|
262
|
+
}
|
|
263
|
+
return bubbles;
|
|
264
|
+
}
|
|
265
|
+
|
|
210
266
|
/**
|
|
211
267
|
* Incremental bubble stream from live token deltas.
|
|
212
268
|
* - done:false = token increment for the active bubble
|
|
213
269
|
* - done:true = that bubble is complete (split off or finalized)
|
|
270
|
+
* - baseIndex offsets indices so later completions continue the turn sequence
|
|
214
271
|
* Stops when a markdown fence starts so tool JSON never leaks.
|
|
215
272
|
*/
|
|
216
|
-
export function createContactBubbleDeltaTracker(onBubbleDelta) {
|
|
273
|
+
export function createContactBubbleDeltaTracker(onBubbleDelta, { baseIndex = 0 } = {}) {
|
|
217
274
|
const emit = typeof onBubbleDelta === 'function' ? onBubbleDelta : null;
|
|
275
|
+
const offset = Number.isFinite(Number(baseIndex)) ? Math.max(0, Number(baseIndex)) : 0;
|
|
218
276
|
let accumulated = '';
|
|
219
277
|
let completedCount = 0;
|
|
220
278
|
let activeEmitted = '';
|
|
@@ -224,7 +282,7 @@ export function createContactBubbleDeltaTracker(onBubbleDelta) {
|
|
|
224
282
|
const push = (index, delta, done) => {
|
|
225
283
|
if (!emit || (typeof delta === 'string' && delta.length === 0 && !done)) return;
|
|
226
284
|
emittedAny = true;
|
|
227
|
-
emit(index, delta, done);
|
|
285
|
+
emit(offset + index, delta, done);
|
|
228
286
|
};
|
|
229
287
|
|
|
230
288
|
const applyVisible = (visible) => {
|
|
@@ -251,6 +309,9 @@ export function createContactBubbleDeltaTracker(onBubbleDelta) {
|
|
|
251
309
|
get stopped() {
|
|
252
310
|
return stopped;
|
|
253
311
|
},
|
|
312
|
+
get baseIndex() {
|
|
313
|
+
return offset;
|
|
314
|
+
},
|
|
254
315
|
pushDelta(delta) {
|
|
255
316
|
if (stopped || typeof delta !== 'string' || !delta) return;
|
|
256
317
|
const next = accumulated + delta;
|
|
@@ -297,8 +358,11 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
297
358
|
globalEventHub = null,
|
|
298
359
|
bubbleGapMs = 0,
|
|
299
360
|
signal = null,
|
|
361
|
+
/** Fixed Agent message length before this contact turn (prior history only). */
|
|
362
|
+
turnMessageStart = 0,
|
|
300
363
|
} = {}) {
|
|
301
364
|
let callSequence = 0;
|
|
365
|
+
const turnStart = Number.isFinite(Number(turnMessageStart)) ? Math.max(0, Number(turnMessageStart)) : 0;
|
|
302
366
|
return (model, context) => {
|
|
303
367
|
const stream = createAssistantMessageEventStream();
|
|
304
368
|
const run = async () => {
|
|
@@ -403,7 +467,9 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
403
467
|
{ 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.'}` },
|
|
404
468
|
);
|
|
405
469
|
}
|
|
406
|
-
|
|
470
|
+
// Continue turn-global bubble indices from already-streamed turn messages.
|
|
471
|
+
const baseIndex = projectStreamedContactTurnBubbles(context.messages, turnStart).length;
|
|
472
|
+
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta, { baseIndex });
|
|
407
473
|
if (parsed.toolCalls.length > 0) {
|
|
408
474
|
const toolCalls = parsed.toolCalls.map((call) => ({
|
|
409
475
|
type: 'toolCall',
|
|
@@ -412,6 +478,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
412
478
|
arguments: call.arguments,
|
|
413
479
|
}));
|
|
414
480
|
const spoken = isContactSpokenPreamble(parsed.chatText) ? parsed.chatText.trim() : '';
|
|
481
|
+
const spokenBubbles = spoken ? splitContactBubbles(spoken) : [];
|
|
415
482
|
const content = spoken
|
|
416
483
|
? [{ type: 'text', text: spoken }, ...toolCalls]
|
|
417
484
|
: toolCalls;
|
|
@@ -419,7 +486,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
419
486
|
...assistantMessage(model, spoken, 'toolUse'),
|
|
420
487
|
content,
|
|
421
488
|
};
|
|
422
|
-
if (
|
|
489
|
+
if (spokenBubbles.length > 0) bubbleTracker.finish(spokenBubbles);
|
|
423
490
|
stream.push({ type: 'start', partial });
|
|
424
491
|
if (spoken) {
|
|
425
492
|
stream.push({ type: 'text_start', contentIndex: 0, partial });
|
|
@@ -440,7 +507,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
440
507
|
const replyBubbles = splitContactBubbles(chatText);
|
|
441
508
|
if (bubbleGapMs > 0 && replyBubbles.length > 1) {
|
|
442
509
|
for (let index = 0; index < replyBubbles.length; index += 1) {
|
|
443
|
-
onBubbleDelta?.(index, replyBubbles[index], true);
|
|
510
|
+
onBubbleDelta?.(baseIndex + index, replyBubbles[index], true);
|
|
444
511
|
if (index < replyBubbles.length - 1) {
|
|
445
512
|
await new Promise((resolve) => setTimeout(resolve, bubbleGapMs));
|
|
446
513
|
}
|
|
@@ -471,22 +538,6 @@ const userMessageText = (message) => {
|
|
|
471
538
|
return (message?.content || []).map((part) => (typeof part?.text === 'string' ? part.text : '')).join('');
|
|
472
539
|
};
|
|
473
540
|
|
|
474
|
-
const assistantTextParts = (message) => (Array.isArray(message?.content) ? message.content : [])
|
|
475
|
-
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
476
|
-
.map((part) => part.text)
|
|
477
|
-
.join('');
|
|
478
|
-
|
|
479
|
-
const extractSpokenPreamble = (messages) => {
|
|
480
|
-
for (const message of Array.isArray(messages) ? messages : []) {
|
|
481
|
-
if (message?.role !== 'assistant') continue;
|
|
482
|
-
const parts = Array.isArray(message.content) ? message.content : [];
|
|
483
|
-
if (!parts.some((part) => part?.type === 'toolCall')) continue;
|
|
484
|
-
const text = assistantTextParts(message).trim();
|
|
485
|
-
if (isContactSpokenPreamble(text)) return text;
|
|
486
|
-
}
|
|
487
|
-
return '';
|
|
488
|
-
};
|
|
489
|
-
|
|
490
541
|
const extractAssistantText = (messages) => {
|
|
491
542
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
492
543
|
const message = messages[index];
|
|
@@ -518,79 +569,56 @@ const extractToolResultText = (messages) => {
|
|
|
518
569
|
return '';
|
|
519
570
|
};
|
|
520
571
|
|
|
521
|
-
/**
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
530
|
-
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
531
|
-
]);
|
|
532
|
-
|
|
533
|
-
/** Card / side-effect tools: never paint English toolText into the transcript. */
|
|
534
|
-
const CARD_SIDE_EFFECT_TOOLS = new Set([
|
|
535
|
-
ASSIGN_SESSION_TOOL_NAME,
|
|
536
|
-
WATCH_SESSION_TOOL_NAME,
|
|
537
|
-
STOP_SESSION_TOOL_NAME,
|
|
538
|
-
CREATE_ASSISTANT_TOOL_NAME,
|
|
539
|
-
SCHEDULE_TASK_TOOL_NAME,
|
|
540
|
-
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
541
|
-
]);
|
|
542
|
-
|
|
543
|
-
/** Lookup payloads for the model only — never paint quoted JSON as a user bubble. */
|
|
544
|
-
const MODEL_ONLY_LOOKUP_TOOLS = new Set([
|
|
545
|
-
READ_SESSION_TOOL_NAME,
|
|
546
|
-
]);
|
|
547
|
-
|
|
548
|
-
const extractContactTurnOutcome = (messages, retried) => {
|
|
549
|
-
const list = Array.isArray(messages) ? messages : [];
|
|
550
|
-
let start = 0;
|
|
551
|
-
if (retried) {
|
|
552
|
-
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
553
|
-
if (list[index]?.role === 'user') {
|
|
554
|
-
start = index;
|
|
555
|
-
break;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
if (start === 0) {
|
|
559
|
-
const toolIndex = list.findIndex((message) => message?.role === 'toolResult');
|
|
560
|
-
if (toolIndex >= 0) start = toolIndex;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
const slice = list.slice(start);
|
|
572
|
+
/**
|
|
573
|
+
* Final turn bubbles reuse the same projection as live onBubbleDelta indices
|
|
574
|
+
* (every spoken preamble + final assistant text; independent messages keep
|
|
575
|
+
* duplicate text). Tool-result-only confirms (never streamed mid-turn) append
|
|
576
|
+
* after that published prefix. Reset collapses to a unique confirm separately.
|
|
577
|
+
*/
|
|
578
|
+
const extractContactTurnOutcome = (messages) => {
|
|
579
|
+
const slice = Array.isArray(messages) ? messages : [];
|
|
564
580
|
const cards = extractContactCardsFromMessages(slice);
|
|
565
581
|
const hasTool = contactTurnHasToolResult(slice);
|
|
566
|
-
|
|
582
|
+
// Same rules as live SSE indices — keep every independent spoken/final bubble.
|
|
583
|
+
const published = projectStreamedContactTurnBubbles(slice, 0);
|
|
567
584
|
const lastToolName = [...slice].reverse().find((message) => message?.role === 'toolResult')?.toolName;
|
|
568
585
|
const coding = isPiCodingToolName(lastToolName);
|
|
569
586
|
const toolText = extractToolResultText(slice);
|
|
570
587
|
const assistant = extractAssistantText(slice);
|
|
571
|
-
|
|
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 = '';
|
|
572
593
|
if (hasTool && !coding) {
|
|
573
594
|
if (TOOL_TEXT_BUBBLE_TOOLS.has(lastToolName)) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
} else if (CARD_SIDE_EFFECT_TOOLS.has(lastToolName)) {
|
|
577
|
-
|
|
578
|
-
// Post-tool assistant text (terminate:false tools) may still surface when no spoken.
|
|
579
|
-
confirm = spoken ? '' : (assistant || '');
|
|
580
|
-
} else if (MODEL_ONLY_LOOKUP_TOOLS.has(lastToolName)) {
|
|
581
|
-
// Quoted transcript stays in the tool result. Keep spoken + post-read reply.
|
|
582
|
-
confirm = assistant || '';
|
|
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 = '';
|
|
583
599
|
} else {
|
|
584
600
|
// list_projects / list_sessions and other app tools keep toolText.
|
|
585
|
-
|
|
601
|
+
toolOnlyConfirm = toolText || '';
|
|
602
|
+
if (!toolOnlyConfirm && published.length === 0) toolOnlyConfirm = assistant || '';
|
|
586
603
|
}
|
|
587
|
-
} else {
|
|
588
|
-
|
|
604
|
+
} else if (hasTool && coding && published.length === 0) {
|
|
605
|
+
toolOnlyConfirm = toolText || '';
|
|
606
|
+
}
|
|
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);
|
|
589
615
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
616
|
+
return {
|
|
617
|
+
text: bubbles.join('\n\n'),
|
|
618
|
+
bubbles,
|
|
619
|
+
cards,
|
|
620
|
+
hasTool,
|
|
621
|
+
};
|
|
594
622
|
};
|
|
595
623
|
|
|
596
624
|
/**
|
|
@@ -670,6 +698,8 @@ export async function runContactTurn({
|
|
|
670
698
|
}
|
|
671
699
|
))
|
|
672
700
|
: [];
|
|
701
|
+
// Fixed turn boundary: every completion and missed-tool retry continues bubble indices from here.
|
|
702
|
+
const turnMessageStart = prior.length;
|
|
673
703
|
|
|
674
704
|
const agent = new AgentImpl({
|
|
675
705
|
toolExecution: 'sequential',
|
|
@@ -687,6 +717,7 @@ export async function runContactTurn({
|
|
|
687
717
|
globalEventHub,
|
|
688
718
|
bubbleGapMs: 280,
|
|
689
719
|
signal,
|
|
720
|
+
turnMessageStart,
|
|
690
721
|
}),
|
|
691
722
|
});
|
|
692
723
|
|
|
@@ -705,9 +736,7 @@ export async function runContactTurn({
|
|
|
705
736
|
const hasRequestedResult = () => agent.state.messages.some((message) => (
|
|
706
737
|
message?.role === 'toolResult' && requested.includes(message.toolName)
|
|
707
738
|
));
|
|
708
|
-
let retried = false;
|
|
709
739
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
710
|
-
retried = true;
|
|
711
740
|
signal?.throwIfAborted();
|
|
712
741
|
await agent.prompt(MISSED_FENCE_RETRY_USER_TEXT);
|
|
713
742
|
signal?.throwIfAborted();
|
|
@@ -719,13 +748,15 @@ export async function runContactTurn({
|
|
|
719
748
|
}
|
|
720
749
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
721
750
|
// Prefer any spoken/assistant text already in the turn over the English failure fallback.
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
751
|
+
// Same turn boundary as live indices (not the retry-user slice alone).
|
|
752
|
+
const missed = extractContactTurnOutcome(agent.state.messages.slice(turnMessageStart));
|
|
753
|
+
const missedBubbles = (Array.isArray(missed.bubbles) ? missed.bubbles : [])
|
|
754
|
+
.map((item) => (typeof item === 'string' ? stripContactToolFences(item).trim() : ''))
|
|
755
|
+
.filter(Boolean);
|
|
756
|
+
if (missedBubbles.length > 0) {
|
|
726
757
|
return {
|
|
727
|
-
text:
|
|
728
|
-
bubbles: missedBubbles
|
|
758
|
+
text: missedBubbles.join('\n\n'),
|
|
759
|
+
bubbles: missedBubbles,
|
|
729
760
|
cards: [],
|
|
730
761
|
thinkingLevel: agent.state.thinkingLevel,
|
|
731
762
|
tools: [...agent.state.tools],
|
|
@@ -739,17 +770,20 @@ export async function runContactTurn({
|
|
|
739
770
|
tools: [...agent.state.tools],
|
|
740
771
|
};
|
|
741
772
|
}
|
|
742
|
-
|
|
743
|
-
const
|
|
773
|
+
// Final bubbles share the turn-boundary projection with live onBubbleDelta indices.
|
|
774
|
+
const outcome = extractContactTurnOutcome(agent.state.messages.slice(turnMessageStart));
|
|
775
|
+
const bubbles = Array.isArray(outcome.bubbles) ? outcome.bubbles : [];
|
|
776
|
+
const text = bubbles.join('\n\n') || outcome.text || '';
|
|
744
777
|
if (contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
745
778
|
const historyCleared = contactTurnClearedChatHistory(agent.state.messages);
|
|
746
779
|
const preferredConfirm = historyCleared
|
|
747
780
|
? CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE
|
|
748
781
|
: NEW_CONVERSATION_CONFIRM_BUBBLE;
|
|
749
|
-
|
|
782
|
+
// Reset is independent: discard mid-turn spokens; keep only the unique confirm.
|
|
783
|
+
const resetBubbles = confirmBubbleAfterContactReset(bubbles, preferredConfirm);
|
|
750
784
|
return {
|
|
751
|
-
text:
|
|
752
|
-
bubbles,
|
|
785
|
+
text: resetBubbles[0] || preferredConfirm,
|
|
786
|
+
bubbles: resetBubbles,
|
|
753
787
|
cards: [],
|
|
754
788
|
reset: true,
|
|
755
789
|
historyCleared,
|
|
@@ -763,9 +797,10 @@ export async function runContactTurn({
|
|
|
763
797
|
error.code = 'upstream_error';
|
|
764
798
|
throw error;
|
|
765
799
|
}
|
|
800
|
+
// Published stream indices are a stable prefix of the final array on normal turns.
|
|
766
801
|
return {
|
|
767
802
|
text,
|
|
768
|
-
bubbles
|
|
803
|
+
bubbles,
|
|
769
804
|
cards: outcome.cards,
|
|
770
805
|
thinkingLevel: agent.state.thinkingLevel,
|
|
771
806
|
tools: [...agent.state.tools],
|