@openchambery/web 1.19.13 → 1.19.14
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/{AgentAvatar-mbvX7YlW.js → AgentAvatar-m1jNq4bX.js} +1 -1
- package/dist/assets/AssistantView-Dncbu_Z2.js +2 -0
- package/dist/assets/{DiagramView-BdI4PT2H.js → DiagramView-CovEl1BY.js} +1 -1
- package/dist/assets/{FadeInOnReveal-moBbL8Ll.js → FadeInOnReveal-UGwClbL_.js} +1 -1
- package/dist/assets/Icon-D1BcR8Tz.js +1 -0
- package/dist/assets/{MarkdownRenderer-CLQ4Z8Z0.js → MarkdownRenderer-CZqok-Cf.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-2ewbRZE2.js → MarkdownRendererImpl-CCfHY-86.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-DXewO7S_.js → MarkstreamRendererImpl-y6QgIUZA.js} +2 -2
- package/dist/assets/MobileDetailNavigation-Ca-d5eZ9.js +1 -0
- package/dist/assets/MobileShareBridge-B4S7BaUj.js +4 -0
- package/dist/assets/MobileSurface-xL5Jby4U.js +2 -0
- package/dist/assets/{MultiRunWindow-C8ukIdLA.js → MultiRunWindow-BeHa-CG6.js} +1 -1
- package/dist/assets/{OnboardingScreen-j0iCyH7f.js → OnboardingScreen-bIWeMLVx.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CDC5B3RH.js → OverlayScrollbar-Bahg_P3r.js} +1 -1
- package/dist/assets/SettingsView-Dn7odtQz.js +25 -0
- package/dist/assets/{SettingsWindow-CSePZ_V8.js → SettingsWindow-C0tWPS5f.js} +1 -1
- package/dist/assets/{TerminalView-BmSIrZ81.js → TerminalView-BnJgC7Qo.js} +1 -1
- package/dist/assets/{ToolOutputDialog-2BYaWLo1.js → ToolOutputDialog-D9S-X8hM.js} +1 -1
- package/dist/assets/{appThemeRegistry-Dk4gBaOu.js → appThemeRegistry-DBrvaJnq.js} +1 -1
- package/dist/assets/{button-xx1UOBA5.js → button-9R-WgVMl.js} +1 -1
- package/dist/assets/{clipboard-CUasgPiF.js → clipboard-GUUNlWIv.js} +1 -1
- package/dist/assets/{collapsible-CACHBRYZ.js → collapsible-Bbz9KiJf.js} +1 -1
- package/dist/assets/{context-menu-CYbm4wBZ.js → context-menu-CSyOhRz8.js} +1 -1
- package/dist/assets/{desktopHosts-DILvrAok.js → desktopHosts-B052a4UK.js} +1 -1
- package/dist/assets/{desktopRelayRestore-t8asCr3m.js → desktopRelayRestore-OjKNXr10.js} +1 -1
- package/dist/assets/{diagram-u0BC9uuY.js → diagram-NdsZih8P.js} +1 -1
- package/dist/assets/{dialog-BwQ3rY_x.js → dialog-DhMGbF_B.js} +1 -1
- package/dist/assets/{es-CRjOwvYM.js → es-BQb6I8gl.js} +2 -2
- package/dist/assets/fileMentionTouchSelection-Dc004xOh.js +4 -0
- package/dist/assets/{fileQueries-Bgly-E8f.js → fileQueries-B_s_B-fH.js} +1 -1
- package/dist/assets/{fr-D9taEN9L.js → fr-BjGouC_l.js} +2 -2
- package/dist/assets/{gitApi-DtPf-h-O.js → gitApi-Cm3JLQqh.js} +3 -3
- package/dist/assets/{input-CI730njs.js → input-Cd_1HyQv.js} +1 -1
- package/dist/assets/insertionBoundaries-CTWYFsbH.js +1 -0
- package/dist/assets/{ja-CYQ5aoWp.js → ja-Cw9zliYY.js} +2 -2
- package/dist/assets/{ko-BwASV0Q0.js → ko-Dyz0Dk9b.js} +2 -2
- package/dist/assets/main-CSqu3MB4.js +169 -0
- package/dist/assets/{main-Ci3JmtpW.js → main-DwMprEyr.js} +2 -2
- package/dist/assets/{markdownHeightCache-DEjGKLv8.js → markdownHeightCache-BL8CUQ0M.js} +1 -1
- package/dist/assets/miniChat-C8bzY_Ye.js +2 -0
- package/dist/assets/mobile-Oy-SRKkr.js +2 -0
- package/dist/assets/{multirun-CmaTeZON.js → multirun-ClcuaiKU.js} +1 -1
- package/dist/assets/{persistence-YbQKzHUQ.js → persistence-DwRBC4zl.js} +2 -2
- package/dist/assets/{pl-DUI1vPil.js → pl-C7EY-dTH.js} +1 -1
- package/dist/assets/platform-DQ5_LNKO.js +2 -0
- package/dist/assets/{projectMeta-cw9nrwpr.js → projectMeta-F93wE2Ye.js} +2 -2
- package/dist/assets/{pt-BR-CZQFJllP.js → pt-BR-B1uHpjFW.js} +2 -2
- package/dist/assets/renderElectronMiniChatApp-rOOhMVlQ.js +2 -0
- package/dist/assets/renderMobileApp-BP_hGwHP.js +28 -0
- package/dist/assets/{runtimeConfig-DI8l0VyZ.js → runtimeConfig-iJ2mTBVN.js} +1 -1
- package/dist/assets/runtimeEndpointReset-ISnhAS3q.js +147 -0
- package/dist/assets/sessionChangeSummary-BAzU2J1H.js +2 -0
- package/dist/assets/sessionLookup-DQQ9-DuP.js +1 -0
- package/dist/assets/{sortable-tabs-strip-Dom6Sthu.js → sortable-tabs-strip-dYW6SH62.js} +1 -1
- package/dist/assets/src-DYeppLfP.css +2 -0
- package/dist/assets/{store-CSnH_QXM.js → store-ClnJdJSj.js} +3 -3
- package/dist/assets/{terminalApi-noa4Cdbh.js → terminalApi-CfgOfnZk.js} +1 -1
- package/dist/assets/{textarea-BfNtUdJE.js → textarea-B9CmN13Z.js} +1 -1
- package/dist/assets/{tooltip-DPxumadF.js → tooltip-DSjr26A_.js} +1 -1
- package/dist/assets/{uk-C1FYZlmA.js → uk-DgG8v2md.js} +2 -2
- package/dist/assets/{url-CVhsI9kR.js → url-CMQwHq4k.js} +1 -1
- package/dist/assets/{useAppFontEffects-BDFUND4v.js → useAppFontEffects--IcU7jyv.js} +43 -44
- package/dist/assets/useDesktopWindowControlsLayout-BqJ5R_T2.js +1 -0
- package/dist/assets/{useEffectiveDirectory-e9HjIqGz.js → useEffectiveDirectory-ChWXV8xM.js} +1 -1
- package/dist/assets/{useFontPreferences-CB-cGo3I.js → useFontPreferences-DCaY8NbU.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DQkgxuzg.js +413 -0
- package/dist/assets/useSessionAutoCleanup-DWWAvQQ0.js +2 -0
- package/dist/assets/{useWorkerHighlightedLines-DTGE0CDs.js → useWorkerHighlightedLines-D2oQszsk.js} +1 -1
- package/dist/assets/{utils-BHTVlVov.js → utils-2ZK8yboQ.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-DDoDDyM9.js → vendor-reactuses-core-BnW6GxJt.js} +1 -1
- package/dist/assets/{zh-CN-C96qP1ip.js → zh-CN-C6OFKjeT.js} +2 -2
- package/dist/assets/{zh-TW-BsRk1Rbb.js → zh-TW-BVP99aMr.js} +2 -2
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/index.js +1 -0
- package/server/lib/assistants/DOCUMENTATION.md +93 -16
- package/server/lib/assistants/assign.js +117 -12
- package/server/lib/assistants/assign.test.js +79 -0
- package/server/lib/assistants/bubbles.js +1 -1
- package/server/lib/assistants/contact-store.js +496 -11
- package/server/lib/assistants/contact-store.test.js +323 -7
- package/server/lib/assistants/contact-tools.js +303 -52
- package/server/lib/assistants/contact-tools.test.js +239 -7
- package/server/lib/assistants/contracts.js +31 -0
- package/server/lib/assistants/harness.js +178 -50
- package/server/lib/assistants/harness.test.js +428 -3
- package/server/lib/assistants/routes.js +3 -0
- package/server/lib/assistants/service.js +586 -43
- package/server/lib/assistants/service.test.js +696 -7
- package/server/lib/llm/DOCUMENTATION.md +2 -0
- package/server/lib/llm/completions.js +4 -0
- package/server/lib/llm/completions.test.js +3 -0
- package/server/lib/llm/generate.js +12 -5
- package/server/lib/llm/generate.test.js +40 -0
- package/server/lib/opencode/feature-routes-runtime.js +5 -0
- package/dist/assets/AssistantView-CKmIS1cS.js +0 -2
- package/dist/assets/Icon-CkzzhaLr.js +0 -1
- package/dist/assets/MobileDetailNavigation-BGaAyuBH.js +0 -1
- package/dist/assets/MobileShareBridge-BAZoyeIP.js +0 -4
- package/dist/assets/MobileSurface-BVb3xNWt.js +0 -2
- package/dist/assets/SettingsView-CkQ61pbr.js +0 -25
- package/dist/assets/insertionBoundaries-BMhzPFft.js +0 -4
- package/dist/assets/main-BjeOhdcG.js +0 -169
- package/dist/assets/miniChat-BYrjYEXJ.js +0 -2
- package/dist/assets/mobile-BVMNnPK2.js +0 -2
- package/dist/assets/platform-DzsGqlGW.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-COwia8Cs.js +0 -2
- package/dist/assets/renderMobileApp-CXwwpY1t.js +0 -28
- package/dist/assets/runtimeEndpointReset-BkAQXmXJ.js +0 -147
- package/dist/assets/sessionChangeSummary-DQDZ5HW-.js +0 -1
- package/dist/assets/sessionLookup-Imc995rW.js +0 -1
- package/dist/assets/src-AA-5nXsF.css +0 -2
- package/dist/assets/useDesktopWindowControlsLayout-C5hZD1co.js +0 -1
- package/dist/assets/useMobileNavigationStore-CYbDxRTM.js +0 -413
- package/dist/assets/useSessionAutoCleanup-BGy9_AhN.js +0 -2
|
@@ -21,9 +21,15 @@ import {
|
|
|
21
21
|
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
22
22
|
NEW_CONVERSATION_TOOL_NAME,
|
|
23
23
|
parseContactToolCalls,
|
|
24
|
+
READ_SESSION_TOOL_NAME,
|
|
24
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,
|
|
25
30
|
stripContactToolFences,
|
|
26
31
|
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
32
|
+
WATCH_SESSION_TOOL_NAME,
|
|
27
33
|
} from './contact-tools.js';
|
|
28
34
|
import {
|
|
29
35
|
createPiCodingRuntime,
|
|
@@ -79,19 +85,72 @@ function createAssistantMessageEventStream() {
|
|
|
79
85
|
};
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
const CONTACT_LANGUAGE_NAMES = {
|
|
89
|
+
en: 'English',
|
|
90
|
+
fr: 'French',
|
|
91
|
+
'zh-CN': 'Simplified Chinese',
|
|
92
|
+
'zh-TW': 'Traditional Chinese',
|
|
93
|
+
uk: 'Ukrainian',
|
|
94
|
+
es: 'Spanish',
|
|
95
|
+
'pt-BR': 'Brazilian Portuguese',
|
|
96
|
+
ko: 'Korean',
|
|
97
|
+
pl: 'Polish',
|
|
98
|
+
ja: 'Japanese',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const CONTACT_LANGUAGE_FALLBACK = "the user's interface language";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Map a UI locale code to a display name for the system prompt. Unknown or
|
|
105
|
+
* missing values fall back to a neutral phrase so `{{LANGUAGE}}` never leaks.
|
|
106
|
+
* Whitelisted only: arbitrary client text must not reach the system prompt.
|
|
107
|
+
*/
|
|
108
|
+
export const resolveContactLanguage = (value) => {
|
|
109
|
+
if (typeof value !== 'string') return CONTACT_LANGUAGE_FALLBACK;
|
|
110
|
+
const code = value.trim();
|
|
111
|
+
if (!code) return CONTACT_LANGUAGE_FALLBACK;
|
|
112
|
+
if (CONTACT_LANGUAGE_NAMES[code]) return CONTACT_LANGUAGE_NAMES[code];
|
|
113
|
+
const match = Object.keys(CONTACT_LANGUAGE_NAMES).find((key) => key.toLowerCase() === code.toLowerCase());
|
|
114
|
+
return match ? CONTACT_LANGUAGE_NAMES[match] : CONTACT_LANGUAGE_FALLBACK;
|
|
115
|
+
};
|
|
116
|
+
|
|
82
117
|
export const CONTACT_SYSTEM_PROMPT = [
|
|
83
118
|
"You are OpenChamber's in-app assistant — a personable contact who can also work in the user's configured project directory.",
|
|
84
119
|
'Reply in short chat bubbles: a few sentences each, separated by a blank line.',
|
|
85
120
|
'Talk like a person in the user\'s language. One short spoken bubble at a time — never a wall of paragraphs.',
|
|
121
|
+
'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.',
|
|
86
122
|
'Never write chain-of-thought, plans, tool names, or English narration of what you will do. The user never sees thinking.',
|
|
87
123
|
'Do not expose tool traces, Activity, or editor actions.',
|
|
88
124
|
'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.',
|
|
89
|
-
'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 schedule_task, 给 X 说一声 means message_assistant, 发卡片 means emit a card via those tools — never ask the user to type /card or /dm.',
|
|
125
|
+
'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.',
|
|
90
126
|
'You receive the registered project catalog every turn. You CAN see those projects. Look them up yourself (fuzzy match label/name/path). Never say you cannot see the registered project list. Never ask for a raw filesystem path when a name matches. If the catalog is empty, tell the user to add a project in Settings.',
|
|
91
|
-
'File and shell work in this working directory uses read, write, edit, and bash. To open a separate Chat coding session: match the project, optionally list_sessions for existing chats, then assign_session with projectPath or 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 ends the turn — do not call it again.',
|
|
92
|
-
'A reply without the tool call does nothing. Never say 已创建, created, scheduled, or
|
|
127
|
+
'File and shell work in this working directory uses read, write, edit, and bash. To open a separate Chat coding session: match the project, optionally list_sessions for existing chats, then assign_session with projectPath or sessionID. To continue an existing chat, assign_session with sessionID plus a coding prompt (omit model args to keep the prior worker model). 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
|
+
'A reply without the tool call does nothing. Never say 已创建, created, scheduled, opened, watched, or stopped unless the tool already returned success.',
|
|
93
129
|
].join(' ');
|
|
94
130
|
|
|
131
|
+
// A workspace response must explicitly finish or ask for missing input. Plain
|
|
132
|
+
// progress prose is not a final answer and must not silently end the pi loop.
|
|
133
|
+
const WORKSPACE_RESPONSE_PROTOCOL = [
|
|
134
|
+
'Workspace response protocol: continue using openchamber-tool calls until the requested work and verification are done.',
|
|
135
|
+
'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 only loads instructions; execute its applicable steps before completing the task.',
|
|
137
|
+
'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
|
+
'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
|
+
'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.',
|
|
140
|
+
].join('\n');
|
|
141
|
+
|
|
142
|
+
const parseFinalResponse = (text) => {
|
|
143
|
+
const match = text.trim().match(/^```openchamber-final\s*([\s\S]*?)```$/u);
|
|
144
|
+
if (!match) return null;
|
|
145
|
+
try {
|
|
146
|
+
const value = JSON.parse(match[1]);
|
|
147
|
+
if (!value || !['complete', 'blocked'].includes(value.status) || typeof value.text !== 'string' || !value.text.trim()) return null;
|
|
148
|
+
return { status: value.status, text: value.text.trim() };
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
95
154
|
const emptyUsage = () => ({
|
|
96
155
|
input: 0,
|
|
97
156
|
output: 0,
|
|
@@ -237,7 +296,9 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
237
296
|
onBubbleDelta = null,
|
|
238
297
|
globalEventHub = null,
|
|
239
298
|
bubbleGapMs = 0,
|
|
299
|
+
signal = null,
|
|
240
300
|
} = {}) {
|
|
301
|
+
let callSequence = 0;
|
|
241
302
|
return (model, context) => {
|
|
242
303
|
const stream = createAssistantMessageEventStream();
|
|
243
304
|
const run = async () => {
|
|
@@ -257,10 +318,13 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
257
318
|
}];
|
|
258
319
|
}
|
|
259
320
|
if (message.role === 'assistant') {
|
|
260
|
-
const content = (message.content || [])
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
321
|
+
const content = (message.content || []).map((part) => {
|
|
322
|
+
if (part?.type === 'text') return part.text;
|
|
323
|
+
if (part?.type !== 'toolCall') return '';
|
|
324
|
+
return '```openchamber-tool\n' + JSON.stringify({
|
|
325
|
+
id: part.id, name: part.name, arguments: part.arguments,
|
|
326
|
+
}) + '\n```';
|
|
327
|
+
}).filter(Boolean).join('\n\n');
|
|
264
328
|
return content ? [{ role: 'assistant', content }] : [];
|
|
265
329
|
}
|
|
266
330
|
if (message.role === 'toolResult') {
|
|
@@ -268,7 +332,6 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
268
332
|
.filter((part) => part?.type === 'text')
|
|
269
333
|
.map((part) => part.text)
|
|
270
334
|
.join('');
|
|
271
|
-
if (!content) return [];
|
|
272
335
|
const toolName = typeof message.toolName === 'string' && message.toolName.trim()
|
|
273
336
|
? message.toolName.trim()
|
|
274
337
|
: 'result';
|
|
@@ -278,7 +341,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
278
341
|
const label = callId
|
|
279
342
|
? `OpenChamber tool result name=${toolName} call=${callId}`
|
|
280
343
|
: `OpenChamber tool result name=${toolName}`;
|
|
281
|
-
return [{ role: 'user', content: `${label}: ${content}` }];
|
|
344
|
+
return [{ role: 'user', content: `${label}: ${JSON.stringify({ isError: message.isError === true, content })}` }];
|
|
282
345
|
}
|
|
283
346
|
return [];
|
|
284
347
|
});
|
|
@@ -290,40 +353,68 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
290
353
|
if (lastUser && fallbackFiles.length > 0 && !Array.isArray(lastUser.parts)) {
|
|
291
354
|
lastUser.parts = fallbackFiles;
|
|
292
355
|
}
|
|
293
|
-
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta);
|
|
294
|
-
const result = await createChatCompletion({
|
|
295
|
-
body: {
|
|
296
|
-
model: `${model.provider === 'openchamber' ? '' : `${model.provider}/`}${model.id}`.replace(/^\//, '') || model.name,
|
|
297
|
-
providerID: typeof model.name === 'string' && model.name.includes('/')
|
|
298
|
-
? model.name.split('/')[0]
|
|
299
|
-
: undefined,
|
|
300
|
-
modelID: model.id,
|
|
301
|
-
messages,
|
|
302
|
-
},
|
|
303
|
-
onTextDelta: (delta) => {
|
|
304
|
-
if (typeof delta !== 'string' || !delta) return;
|
|
305
|
-
// Do not live-paint raw tokens: they are often chain-of-thought
|
|
306
|
-
// and would appear then vanish after parse/persist.
|
|
307
|
-
if (typeof onTextDelta === 'function') onTextDelta(delta);
|
|
308
|
-
},
|
|
309
|
-
globalEventHub,
|
|
310
|
-
});
|
|
311
|
-
const text = result?.completion?.choices?.[0]?.message?.content
|
|
312
|
-
?? result?.text
|
|
313
|
-
?? '';
|
|
314
356
|
const allowedNames = (context.tools || []).map((tool) => tool?.name).filter(Boolean);
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
357
|
+
const requiresFinal = allowedNames.some(isPiCodingToolName);
|
|
358
|
+
let text;
|
|
359
|
+
let parsed;
|
|
360
|
+
let finalText;
|
|
361
|
+
let rejectedCodingCalls = [];
|
|
362
|
+
// Repair only the rejected response: no tools have executed from it.
|
|
363
|
+
// Real results stay in messages, so already completed work is not replayed.
|
|
364
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
365
|
+
signal?.throwIfAborted();
|
|
366
|
+
const result = await createChatCompletion({
|
|
367
|
+
signal,
|
|
368
|
+
body: {
|
|
369
|
+
model: `${model.provider === 'openchamber' ? '' : `${model.provider}/`}${model.id}`.replace(/^\//, '') || model.name,
|
|
370
|
+
providerID: typeof model.name === 'string' && model.name.includes('/')
|
|
371
|
+
? model.name.split('/')[0]
|
|
372
|
+
: undefined,
|
|
373
|
+
modelID: model.id,
|
|
374
|
+
messages: [...messages],
|
|
375
|
+
},
|
|
376
|
+
onTextDelta: (delta) => {
|
|
377
|
+
if (typeof delta === 'string' && delta && typeof onTextDelta === 'function') onTextDelta(delta);
|
|
378
|
+
},
|
|
379
|
+
globalEventHub,
|
|
380
|
+
});
|
|
381
|
+
signal?.throwIfAborted();
|
|
382
|
+
text = result?.completion?.choices?.[0]?.message?.content ?? result?.text ?? '';
|
|
383
|
+
const finalResponse = parseFinalResponse(text);
|
|
384
|
+
finalText = finalResponse?.text ?? null;
|
|
385
|
+
// A final answer is opaque text; quoted tool examples in it cannot run.
|
|
386
|
+
parsed = finalText === null ? parseContactToolCalls(text, allowedNames) : { toolCalls: [], chatText: finalText };
|
|
387
|
+
const mixedBatch = parsed.toolCalls.length > 1 && parsed.toolCalls.some((call) => !isPiCodingToolName(call.name));
|
|
388
|
+
const malformedFinal = finalText === null && /```openchamber-final/u.test(parsed.chatText);
|
|
389
|
+
const skippedRejectedWork = rejectedCodingCalls.length > 0 && finalResponse?.status === 'complete';
|
|
390
|
+
const forgedTranscript = finalText === null && /(?:^|\n)\s*(?:(?:User|Assistant|Tool):|OpenChamber tool result name=)/iu.test(parsed.chatText);
|
|
391
|
+
const protocolError = parsed.protocolError
|
|
392
|
+
|| (forgedTranscript ? 'Do not simulate user messages, assistant turns, or tool results. Output only your next real tool call and wait for execution.' : '')
|
|
393
|
+
|| (skippedRejectedWork ? 'Previously rejected coding calls have not executed. Issue the remaining tool calls, or report a blocked outcome; do not declare them complete.' : '')
|
|
394
|
+
|| (mixedBatch ? 'Send OpenChamber operation tools one at a time, without other calls in the same response.' : '')
|
|
395
|
+
|| (malformedFinal ? 'The final declaration must be a single valid openchamber-final JSON fence with status and text, without other content.' : '')
|
|
396
|
+
|| (requiresFinal && parsed.toolCalls.length === 0 && finalText === null ? 'A workspace response needs an actual tool call or an explicit final declaration.' : '');
|
|
397
|
+
if (!protocolError) break;
|
|
398
|
+
if (attempt >= 2) throw new Error('Assistant response protocol failed after two corrections; the task was not confirmed complete.');
|
|
399
|
+
const rejected = parsed.toolCalls.filter((call) => isPiCodingToolName(call.name));
|
|
400
|
+
if (rejected.length > 0) rejectedCodingCalls = rejected.map(({ name, arguments: args }) => ({ name, arguments: args }));
|
|
401
|
+
// Never replay rejected prose: it may contain fabricated user/tool results.
|
|
402
|
+
messages.push(
|
|
403
|
+
{ 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
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta);
|
|
407
|
+
if (parsed.toolCalls.length > 0) {
|
|
408
|
+
const toolCalls = parsed.toolCalls.map((call) => ({
|
|
318
409
|
type: 'toolCall',
|
|
319
|
-
id: `call_${Date.now().toString(36)}`,
|
|
320
|
-
name:
|
|
321
|
-
arguments:
|
|
322
|
-
};
|
|
410
|
+
id: `call_${Date.now().toString(36)}_${++callSequence}`,
|
|
411
|
+
name: call.name,
|
|
412
|
+
arguments: call.arguments,
|
|
413
|
+
}));
|
|
323
414
|
const spoken = isContactSpokenPreamble(parsed.chatText) ? parsed.chatText.trim() : '';
|
|
324
415
|
const content = spoken
|
|
325
|
-
? [{ type: 'text', text: spoken },
|
|
326
|
-
:
|
|
416
|
+
? [{ type: 'text', text: spoken }, ...toolCalls]
|
|
417
|
+
: toolCalls;
|
|
327
418
|
const partial = {
|
|
328
419
|
...assistantMessage(model, spoken, 'toolUse'),
|
|
329
420
|
content,
|
|
@@ -335,15 +426,17 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
335
426
|
stream.push({ type: 'text_delta', contentIndex: 0, delta: spoken, partial });
|
|
336
427
|
stream.push({ type: 'text_end', contentIndex: 0, content: spoken, partial });
|
|
337
428
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
429
|
+
toolCalls.forEach((toolCall, index) => {
|
|
430
|
+
const toolIndex = index + (spoken ? 1 : 0);
|
|
431
|
+
stream.push({ type: 'toolcall_start', contentIndex: toolIndex, partial });
|
|
432
|
+
stream.push({ type: 'toolcall_delta', contentIndex: toolIndex, delta: JSON.stringify(toolCall.arguments), partial });
|
|
433
|
+
stream.push({ type: 'toolcall_end', contentIndex: toolIndex, toolCall, partial });
|
|
434
|
+
});
|
|
342
435
|
stream.push({ type: 'done', reason: 'toolUse', message: partial });
|
|
343
436
|
stream.end(partial);
|
|
344
437
|
return;
|
|
345
438
|
}
|
|
346
|
-
const chatText =
|
|
439
|
+
const chatText = finalText ?? parsed.chatText;
|
|
347
440
|
const replyBubbles = splitContactBubbles(chatText);
|
|
348
441
|
if (bubbleGapMs > 0 && replyBubbles.length > 1) {
|
|
349
442
|
for (let index = 0; index < replyBubbles.length; index += 1) {
|
|
@@ -363,7 +456,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
363
456
|
stream.push({ type: 'done', reason: 'stop', message: partial });
|
|
364
457
|
stream.end(partial);
|
|
365
458
|
} catch (error) {
|
|
366
|
-
const failed = assistantMessage(model, '', 'error', error?.message || 'upstream_error');
|
|
459
|
+
const failed = assistantMessage(model, '', signal?.aborted ? 'aborted' : 'error', error?.message || 'upstream_error');
|
|
367
460
|
stream.push({ type: 'error', reason: 'error', error: failed });
|
|
368
461
|
stream.end(failed);
|
|
369
462
|
}
|
|
@@ -427,6 +520,10 @@ const extractToolResultText = (messages) => {
|
|
|
427
520
|
|
|
428
521
|
/** Tool confirms that are themselves the user-facing bubble (no card). */
|
|
429
522
|
const TOOL_TEXT_BUBBLE_TOOLS = new Set([
|
|
523
|
+
STOP_SESSION_TOOL_NAME,
|
|
524
|
+
STEER_SESSION_TOOL_NAME,
|
|
525
|
+
ARCHIVE_SESSION_TOOL_NAME,
|
|
526
|
+
DELETE_SESSION_TOOL_NAME,
|
|
430
527
|
NEW_CONVERSATION_TOOL_NAME,
|
|
431
528
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
432
529
|
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
@@ -436,11 +533,18 @@ const TOOL_TEXT_BUBBLE_TOOLS = new Set([
|
|
|
436
533
|
/** Card / side-effect tools: never paint English toolText into the transcript. */
|
|
437
534
|
const CARD_SIDE_EFFECT_TOOLS = new Set([
|
|
438
535
|
ASSIGN_SESSION_TOOL_NAME,
|
|
536
|
+
WATCH_SESSION_TOOL_NAME,
|
|
537
|
+
STOP_SESSION_TOOL_NAME,
|
|
439
538
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
440
539
|
SCHEDULE_TASK_TOOL_NAME,
|
|
441
540
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
442
541
|
]);
|
|
443
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
|
+
|
|
444
548
|
const extractContactTurnOutcome = (messages, retried) => {
|
|
445
549
|
const list = Array.isArray(messages) ? messages : [];
|
|
446
550
|
let start = 0;
|
|
@@ -473,6 +577,9 @@ const extractContactTurnOutcome = (messages, retried) => {
|
|
|
473
577
|
// Spoken preamble only. Card is enough when present; never English toolText.
|
|
474
578
|
// Post-tool assistant text (terminate:false tools) may still surface when no spoken.
|
|
475
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 || '';
|
|
476
583
|
} else {
|
|
477
584
|
// list_projects / list_sessions and other app tools keep toolText.
|
|
478
585
|
confirm = toolText || assistant;
|
|
@@ -504,34 +611,45 @@ export async function runContactTurn({
|
|
|
504
611
|
tools = [],
|
|
505
612
|
projects = [],
|
|
506
613
|
connectedModels = [],
|
|
614
|
+
modelPreferences = null,
|
|
615
|
+
modelCatalogAvailable = true,
|
|
507
616
|
onTextDelta = null,
|
|
508
617
|
onBubbleDelta = null,
|
|
509
618
|
globalEventHub = null,
|
|
510
619
|
skillHomeDir,
|
|
620
|
+
language = '',
|
|
511
621
|
AgentImpl = Agent,
|
|
622
|
+
signal = null,
|
|
623
|
+
readOnly = false,
|
|
512
624
|
}) {
|
|
513
625
|
const providerID = assistant.providerID;
|
|
514
626
|
const modelID = assistant.modelID;
|
|
515
627
|
const cwd = resolveAssistantCwd(assistant);
|
|
516
628
|
const contactTools = Array.isArray(tools)
|
|
517
|
-
? tools.filter((tool) => tool && typeof tool.name === 'string' && !isPiCodingToolName(tool.name))
|
|
629
|
+
? tools.filter((tool) => tool && typeof tool.name === 'string' && !isPiCodingToolName(tool.name) && (!readOnly || ['list_projects', 'list_sessions', 'read_session', 'get_assistant_settings'].includes(tool.name)))
|
|
518
630
|
: [];
|
|
519
631
|
let runtime = null;
|
|
632
|
+
let removeAbortListener = null;
|
|
520
633
|
try {
|
|
521
|
-
|
|
634
|
+
signal?.throwIfAborted();
|
|
635
|
+
if (cwd && !readOnly) runtime = await createPiCodingRuntime(cwd, { homeDir: skillHomeDir });
|
|
522
636
|
const codingTools = Array.isArray(runtime?.tools) ? runtime.tools : [];
|
|
523
637
|
const systemPrompt = [
|
|
524
638
|
CONTACT_SYSTEM_PROMPT,
|
|
639
|
+
'Conversation history is a bounded recent window, not guaranteed complete memory. OpenChamber card context records contain actual session identifiers and status; use them to resolve references to earlier work. Never claim to remember omitted details or repeat completed work because older context is missing. Ask for the missing detail when needed.',
|
|
640
|
+
readOnly ? 'This turn is a background result notification. Only read-only lookup tools are available. Never create or continue work; follow the latest user constraints and report the result briefly.' : '',
|
|
525
641
|
formatPiCodingPrompt({
|
|
526
642
|
cwd: runtime?.cwd,
|
|
527
643
|
skillsPrompt: runtime?.skillsPrompt,
|
|
528
644
|
tools: codingTools,
|
|
529
645
|
}),
|
|
530
646
|
formatRegisteredProjectsPrompt(projects),
|
|
531
|
-
formatConnectedModelsPrompt(connectedModels),
|
|
647
|
+
formatConnectedModelsPrompt(connectedModels, { preferences: modelPreferences, catalogAvailable: modelCatalogAvailable }),
|
|
532
648
|
formatContactToolsPrompt(contactTools),
|
|
533
649
|
assistant.defaultPrompt,
|
|
534
|
-
|
|
650
|
+
codingTools.length > 0 ? WORKSPACE_RESPONSE_PROTOCOL : '',
|
|
651
|
+
].filter((value) => typeof value === 'string' && value.trim()).join('\n\n')
|
|
652
|
+
.replaceAll('{{LANGUAGE}}', resolveContactLanguage(language));
|
|
535
653
|
const model = createContactModel(providerID, modelID);
|
|
536
654
|
// streamFn receives the model; completions needs providerID/modelID.
|
|
537
655
|
model.name = `${providerID}/${modelID}`;
|
|
@@ -554,6 +672,7 @@ export async function runContactTurn({
|
|
|
554
672
|
: [];
|
|
555
673
|
|
|
556
674
|
const agent = new AgentImpl({
|
|
675
|
+
toolExecution: 'sequential',
|
|
557
676
|
initialState: {
|
|
558
677
|
systemPrompt,
|
|
559
678
|
model,
|
|
@@ -567,10 +686,16 @@ export async function runContactTurn({
|
|
|
567
686
|
onBubbleDelta,
|
|
568
687
|
globalEventHub,
|
|
569
688
|
bubbleGapMs: 280,
|
|
689
|
+
signal,
|
|
570
690
|
}),
|
|
571
691
|
});
|
|
572
692
|
|
|
693
|
+
const onAbort = () => agent.abort();
|
|
694
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
695
|
+
removeAbortListener = () => signal?.removeEventListener('abort', onAbort);
|
|
696
|
+
signal?.throwIfAborted();
|
|
573
697
|
await agent.prompt(userText);
|
|
698
|
+
signal?.throwIfAborted();
|
|
574
699
|
if (agent.state.errorMessage) {
|
|
575
700
|
const error = new Error(agent.state.errorMessage);
|
|
576
701
|
error.code = 'upstream_error';
|
|
@@ -583,7 +708,9 @@ export async function runContactTurn({
|
|
|
583
708
|
let retried = false;
|
|
584
709
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
585
710
|
retried = true;
|
|
711
|
+
signal?.throwIfAborted();
|
|
586
712
|
await agent.prompt(MISSED_FENCE_RETRY_USER_TEXT);
|
|
713
|
+
signal?.throwIfAborted();
|
|
587
714
|
if (agent.state.errorMessage) {
|
|
588
715
|
const error = new Error(agent.state.errorMessage);
|
|
589
716
|
error.code = 'upstream_error';
|
|
@@ -613,7 +740,7 @@ export async function runContactTurn({
|
|
|
613
740
|
};
|
|
614
741
|
}
|
|
615
742
|
const outcome = extractContactTurnOutcome(agent.state.messages, retried);
|
|
616
|
-
const text =
|
|
743
|
+
const text = outcome.text;
|
|
617
744
|
if (contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
618
745
|
const historyCleared = contactTurnClearedChatHistory(agent.state.messages);
|
|
619
746
|
const preferredConfirm = historyCleared
|
|
@@ -644,6 +771,7 @@ export async function runContactTurn({
|
|
|
644
771
|
tools: [...agent.state.tools],
|
|
645
772
|
};
|
|
646
773
|
} finally {
|
|
774
|
+
removeAbortListener?.();
|
|
647
775
|
try {
|
|
648
776
|
await runtime?.close?.();
|
|
649
777
|
} catch {
|