@openchambery/web 1.19.13-beta.4 → 1.19.13-beta.6
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-Bk-m9q6w.js} +1 -1
- package/dist/assets/AssistantView-7NzDL0Ft.js +2 -0
- package/dist/assets/{DiagramView-qyOlOyET.js → DiagramView-CAbNIt3t.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-DJORbaAh.js → MarkdownRenderer-BPYs05iK.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-Baw1_E_b.js → MarkdownRendererImpl-DMOxz3F2.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-C3y7cRux.js → MarkstreamRendererImpl-igQBbbc_.js} +2 -2
- package/dist/assets/MobileDetailNavigation-BqXkyZTu.js +1 -0
- package/dist/assets/MobileShareBridge-XVaxj2S6.js +4 -0
- package/dist/assets/MobileSurface-6zONMJbZ.js +2 -0
- package/dist/assets/{MultiRunWindow-CnzvP7OW.js → MultiRunWindow-DK4IMgXD.js} +1 -1
- package/dist/assets/{OnboardingScreen-j0iCyH7f.js → OnboardingScreen-Ql6mH4XV.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CDC5B3RH.js → OverlayScrollbar-Bahg_P3r.js} +1 -1
- package/dist/assets/{SettingsView-Gerbi-eT.js → SettingsView-DyV0qt_2.js} +8 -8
- package/dist/assets/{SettingsWindow-Czkt-v7v.js → SettingsWindow-D6BMtTR9.js} +1 -1
- package/dist/assets/{TerminalView-C3PKUnFu.js → TerminalView-D24PhNyX.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Nv1AVjeU.js → ToolOutputDialog-uKzrTqq_.js} +1 -1
- package/dist/assets/{appThemeRegistry-eohB6OfO.js → appThemeRegistry-CvGPIE7-.js} +1 -1
- package/dist/assets/{button-xx1UOBA5.js → button-9R-WgVMl.js} +1 -1
- package/dist/assets/{clipboard-CUasgPiF.js → clipboard-CzXos-TI.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-gtFLyacK.js} +1 -1
- package/dist/assets/{es-CRjOwvYM.js → es-BtC17YQH.js} +1 -1
- package/dist/assets/fileMentionTouchSelection-DstJbYAw.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-DPohCCUO.js} +1 -1
- package/dist/assets/{gitApi-CrP1R39C.js → gitApi-ClM_B3fS.js} +3 -3
- package/dist/assets/{input-CI730njs.js → input-Cd_1HyQv.js} +1 -1
- package/dist/assets/insertionBoundaries-RilpwKtQ.js +1 -0
- package/dist/assets/{ja-CYQ5aoWp.js → ja-CflPxUNi.js} +1 -1
- package/dist/assets/{ko-BwASV0Q0.js → ko-CuSU80xd.js} +1 -1
- package/dist/assets/main-C2wx8T4t.js +169 -0
- package/dist/assets/{main-Bjgj-x5l.js → main-uBrYU2q0.js} +2 -2
- package/dist/assets/{markdownHeightCache-DEjGKLv8.js → markdownHeightCache-BL8CUQ0M.js} +1 -1
- package/dist/assets/miniChat-Bt2HBpWq.js +2 -0
- package/dist/assets/mobile-CpcbfIT1.js +2 -0
- package/dist/assets/{multirun-IUfGqjJa.js → multirun-FNfO4hCT.js} +1 -1
- package/dist/assets/{persistence-YbQKzHUQ.js → persistence-DwRBC4zl.js} +2 -2
- package/dist/assets/{pl-DUI1vPil.js → pl-eJvVbQ74.js} +1 -1
- package/dist/assets/platform-DQ5_LNKO.js +2 -0
- package/dist/assets/{projectMeta-B94Ha79_.js → projectMeta-Cfq8wYwb.js} +2 -2
- package/dist/assets/{pt-BR-CZQFJllP.js → pt-BR-DSejAksY.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-DwWmaqPt.js → renderElectronMiniChatApp-CA5VKfGo.js} +2 -2
- package/dist/assets/renderMobileApp-De8AWiA6.js +28 -0
- package/dist/assets/{runtimeConfig-DhAd_g4W.js → runtimeConfig-BpsTIe2G.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CjT4jAvZ.js +147 -0
- package/dist/assets/sessionChangeSummary-DKJxz5er.js +2 -0
- package/dist/assets/sessionLookup-CBT9qysS.js +1 -0
- package/dist/assets/{sortable-tabs-strip-Dom6Sthu.js → sortable-tabs-strip-BSzFYDAO.js} +1 -1
- package/dist/assets/src-BdO1hC7q.css +2 -0
- package/dist/assets/{store-CSnH_QXM.js → store-DHdMj3y4.js} +2 -2
- package/dist/assets/{terminalApi-noa4Cdbh.js → terminalApi-CfgOfnZk.js} +1 -1
- package/dist/assets/{textarea-BfNtUdJE.js → textarea-DE1UrdtK.js} +1 -1
- package/dist/assets/{tooltip-DPxumadF.js → tooltip-DSjr26A_.js} +1 -1
- package/dist/assets/{uk-C1FYZlmA.js → uk-DV9OgOAg.js} +1 -1
- package/dist/assets/{url-CVhsI9kR.js → url-CMQwHq4k.js} +1 -1
- package/dist/assets/{useAppFontEffects-BO7HWYlJ.js → useAppFontEffects-BD0BG-np.js} +43 -44
- package/dist/assets/{useDesktopWindowControlsLayout-BAuvqFxV.js → useDesktopWindowControlsLayout-CF2GHw9J.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-B9GzC3CE.js → useEffectiveDirectory-Tz4YgZHt.js} +1 -1
- package/dist/assets/{useFontPreferences-CB-cGo3I.js → useFontPreferences-DCaY8NbU.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BlYOTQfq.js +413 -0
- package/dist/assets/{useSessionAutoCleanup-DSAkV_JD.js → useSessionAutoCleanup-VgkhRtrf.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-BW-HxZ9_.js → useWorkerHighlightedLines-DT0cJd4V.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-92n_rGTt.js} +1 -1
- package/dist/assets/{zh-TW-BsRk1Rbb.js → zh-TW-CwVxwJCj.js} +1 -1
- 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 +169 -50
- package/server/lib/assistants/harness.test.js +355 -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-4rOhnCbj.js +0 -2
- package/dist/assets/Icon-CkzzhaLr.js +0 -1
- package/dist/assets/MobileDetailNavigation-Cy-LwJ5M.js +0 -1
- package/dist/assets/MobileShareBridge-CTai3NEM.js +0 -4
- package/dist/assets/MobileSurface-CYg5Flgz.js +0 -2
- package/dist/assets/insertionBoundaries-C8jXd3NT.js +0 -4
- package/dist/assets/main-DZg9T7tk.js +0 -169
- package/dist/assets/miniChat-DUFEC4jl.js +0 -2
- package/dist/assets/mobile-BqOSxf04.js +0 -2
- package/dist/assets/platform-DzsGqlGW.js +0 -2
- package/dist/assets/renderMobileApp-ByS1wjSm.js +0 -28
- package/dist/assets/runtimeEndpointReset-Dk_Xpe6y.js +0 -147
- package/dist/assets/sessionChangeSummary-DQDZ5HW-.js +0 -1
- package/dist/assets/sessionLookup-DZcwnQYW.js +0 -1
- package/dist/assets/src-AA-5nXsF.css +0 -2
- package/dist/assets/useMobileNavigationStore-Dqe-sz0u.js +0 -413
|
@@ -22,8 +22,13 @@ import {
|
|
|
22
22
|
NEW_CONVERSATION_TOOL_NAME,
|
|
23
23
|
parseContactToolCalls,
|
|
24
24
|
SCHEDULE_TASK_TOOL_NAME,
|
|
25
|
+
STOP_SESSION_TOOL_NAME,
|
|
26
|
+
STEER_SESSION_TOOL_NAME,
|
|
27
|
+
ARCHIVE_SESSION_TOOL_NAME,
|
|
28
|
+
DELETE_SESSION_TOOL_NAME,
|
|
25
29
|
stripContactToolFences,
|
|
26
30
|
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
31
|
+
WATCH_SESSION_TOOL_NAME,
|
|
27
32
|
} from './contact-tools.js';
|
|
28
33
|
import {
|
|
29
34
|
createPiCodingRuntime,
|
|
@@ -79,19 +84,72 @@ function createAssistantMessageEventStream() {
|
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
const CONTACT_LANGUAGE_NAMES = {
|
|
88
|
+
en: 'English',
|
|
89
|
+
fr: 'French',
|
|
90
|
+
'zh-CN': 'Simplified Chinese',
|
|
91
|
+
'zh-TW': 'Traditional Chinese',
|
|
92
|
+
uk: 'Ukrainian',
|
|
93
|
+
es: 'Spanish',
|
|
94
|
+
'pt-BR': 'Brazilian Portuguese',
|
|
95
|
+
ko: 'Korean',
|
|
96
|
+
pl: 'Polish',
|
|
97
|
+
ja: 'Japanese',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const CONTACT_LANGUAGE_FALLBACK = "the user's interface language";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Map a UI locale code to a display name for the system prompt. Unknown or
|
|
104
|
+
* missing values fall back to a neutral phrase so `{{LANGUAGE}}` never leaks.
|
|
105
|
+
* Whitelisted only: arbitrary client text must not reach the system prompt.
|
|
106
|
+
*/
|
|
107
|
+
export const resolveContactLanguage = (value) => {
|
|
108
|
+
if (typeof value !== 'string') return CONTACT_LANGUAGE_FALLBACK;
|
|
109
|
+
const code = value.trim();
|
|
110
|
+
if (!code) return CONTACT_LANGUAGE_FALLBACK;
|
|
111
|
+
if (CONTACT_LANGUAGE_NAMES[code]) return CONTACT_LANGUAGE_NAMES[code];
|
|
112
|
+
const match = Object.keys(CONTACT_LANGUAGE_NAMES).find((key) => key.toLowerCase() === code.toLowerCase());
|
|
113
|
+
return match ? CONTACT_LANGUAGE_NAMES[match] : CONTACT_LANGUAGE_FALLBACK;
|
|
114
|
+
};
|
|
115
|
+
|
|
82
116
|
export const CONTACT_SYSTEM_PROMPT = [
|
|
83
117
|
"You are OpenChamber's in-app assistant — a personable contact who can also work in the user's configured project directory.",
|
|
84
118
|
'Reply in short chat bubbles: a few sentences each, separated by a blank line.',
|
|
85
119
|
'Talk like a person in the user\'s language. One short spoken bubble at a time — never a wall of paragraphs.',
|
|
120
|
+
'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
121
|
'Never write chain-of-thought, plans, tool names, or English narration of what you will do. The user never sees thinking.',
|
|
87
122
|
'Do not expose tool traces, Activity, or editor actions.',
|
|
88
123
|
'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.',
|
|
124
|
+
'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
125
|
'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
|
|
126
|
+
'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.',
|
|
127
|
+
'A reply without the tool call does nothing. Never say 已创建, created, scheduled, opened, watched, or stopped unless the tool already returned success.',
|
|
93
128
|
].join(' ');
|
|
94
129
|
|
|
130
|
+
// A workspace response must explicitly finish or ask for missing input. Plain
|
|
131
|
+
// progress prose is not a final answer and must not silently end the pi loop.
|
|
132
|
+
const WORKSPACE_RESPONSE_PROTOCOL = [
|
|
133
|
+
'Workspace response protocol: continue using openchamber-tool calls until the requested work and verification are done.',
|
|
134
|
+
'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.',
|
|
135
|
+
'Reading a skill only loads instructions; execute its applicable steps before completing the task.',
|
|
136
|
+
'To finish, reply with only an openchamber-final JSON fence: ```openchamber-final\n{"status":"complete","text":"Your final answer in the user language"}\n```.',
|
|
137
|
+
'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.',
|
|
138
|
+
'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.',
|
|
139
|
+
].join('\n');
|
|
140
|
+
|
|
141
|
+
const parseFinalResponse = (text) => {
|
|
142
|
+
const match = text.trim().match(/^```openchamber-final\s*([\s\S]*?)```$/u);
|
|
143
|
+
if (!match) return null;
|
|
144
|
+
try {
|
|
145
|
+
const value = JSON.parse(match[1]);
|
|
146
|
+
if (!value || !['complete', 'blocked'].includes(value.status) || typeof value.text !== 'string' || !value.text.trim()) return null;
|
|
147
|
+
return { status: value.status, text: value.text.trim() };
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
95
153
|
const emptyUsage = () => ({
|
|
96
154
|
input: 0,
|
|
97
155
|
output: 0,
|
|
@@ -237,7 +295,9 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
237
295
|
onBubbleDelta = null,
|
|
238
296
|
globalEventHub = null,
|
|
239
297
|
bubbleGapMs = 0,
|
|
298
|
+
signal = null,
|
|
240
299
|
} = {}) {
|
|
300
|
+
let callSequence = 0;
|
|
241
301
|
return (model, context) => {
|
|
242
302
|
const stream = createAssistantMessageEventStream();
|
|
243
303
|
const run = async () => {
|
|
@@ -257,10 +317,13 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
257
317
|
}];
|
|
258
318
|
}
|
|
259
319
|
if (message.role === 'assistant') {
|
|
260
|
-
const content = (message.content || [])
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
320
|
+
const content = (message.content || []).map((part) => {
|
|
321
|
+
if (part?.type === 'text') return part.text;
|
|
322
|
+
if (part?.type !== 'toolCall') return '';
|
|
323
|
+
return '```openchamber-tool\n' + JSON.stringify({
|
|
324
|
+
id: part.id, name: part.name, arguments: part.arguments,
|
|
325
|
+
}) + '\n```';
|
|
326
|
+
}).filter(Boolean).join('\n\n');
|
|
264
327
|
return content ? [{ role: 'assistant', content }] : [];
|
|
265
328
|
}
|
|
266
329
|
if (message.role === 'toolResult') {
|
|
@@ -268,7 +331,6 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
268
331
|
.filter((part) => part?.type === 'text')
|
|
269
332
|
.map((part) => part.text)
|
|
270
333
|
.join('');
|
|
271
|
-
if (!content) return [];
|
|
272
334
|
const toolName = typeof message.toolName === 'string' && message.toolName.trim()
|
|
273
335
|
? message.toolName.trim()
|
|
274
336
|
: 'result';
|
|
@@ -278,7 +340,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
278
340
|
const label = callId
|
|
279
341
|
? `OpenChamber tool result name=${toolName} call=${callId}`
|
|
280
342
|
: `OpenChamber tool result name=${toolName}`;
|
|
281
|
-
return [{ role: 'user', content: `${label}: ${content}` }];
|
|
343
|
+
return [{ role: 'user', content: `${label}: ${JSON.stringify({ isError: message.isError === true, content })}` }];
|
|
282
344
|
}
|
|
283
345
|
return [];
|
|
284
346
|
});
|
|
@@ -290,40 +352,68 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
290
352
|
if (lastUser && fallbackFiles.length > 0 && !Array.isArray(lastUser.parts)) {
|
|
291
353
|
lastUser.parts = fallbackFiles;
|
|
292
354
|
}
|
|
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
355
|
const allowedNames = (context.tools || []).map((tool) => tool?.name).filter(Boolean);
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
356
|
+
const requiresFinal = allowedNames.some(isPiCodingToolName);
|
|
357
|
+
let text;
|
|
358
|
+
let parsed;
|
|
359
|
+
let finalText;
|
|
360
|
+
let rejectedCodingCalls = [];
|
|
361
|
+
// Repair only the rejected response: no tools have executed from it.
|
|
362
|
+
// Real results stay in messages, so already completed work is not replayed.
|
|
363
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
364
|
+
signal?.throwIfAborted();
|
|
365
|
+
const result = await createChatCompletion({
|
|
366
|
+
signal,
|
|
367
|
+
body: {
|
|
368
|
+
model: `${model.provider === 'openchamber' ? '' : `${model.provider}/`}${model.id}`.replace(/^\//, '') || model.name,
|
|
369
|
+
providerID: typeof model.name === 'string' && model.name.includes('/')
|
|
370
|
+
? model.name.split('/')[0]
|
|
371
|
+
: undefined,
|
|
372
|
+
modelID: model.id,
|
|
373
|
+
messages: [...messages],
|
|
374
|
+
},
|
|
375
|
+
onTextDelta: (delta) => {
|
|
376
|
+
if (typeof delta === 'string' && delta && typeof onTextDelta === 'function') onTextDelta(delta);
|
|
377
|
+
},
|
|
378
|
+
globalEventHub,
|
|
379
|
+
});
|
|
380
|
+
signal?.throwIfAborted();
|
|
381
|
+
text = result?.completion?.choices?.[0]?.message?.content ?? result?.text ?? '';
|
|
382
|
+
const finalResponse = parseFinalResponse(text);
|
|
383
|
+
finalText = finalResponse?.text ?? null;
|
|
384
|
+
// A final answer is opaque text; quoted tool examples in it cannot run.
|
|
385
|
+
parsed = finalText === null ? parseContactToolCalls(text, allowedNames) : { toolCalls: [], chatText: finalText };
|
|
386
|
+
const mixedBatch = parsed.toolCalls.length > 1 && parsed.toolCalls.some((call) => !isPiCodingToolName(call.name));
|
|
387
|
+
const malformedFinal = finalText === null && /```openchamber-final/u.test(parsed.chatText);
|
|
388
|
+
const skippedRejectedWork = rejectedCodingCalls.length > 0 && finalResponse?.status === 'complete';
|
|
389
|
+
const forgedTranscript = finalText === null && /(?:^|\n)\s*(?:(?:User|Assistant|Tool):|OpenChamber tool result name=)/iu.test(parsed.chatText);
|
|
390
|
+
const protocolError = parsed.protocolError
|
|
391
|
+
|| (forgedTranscript ? 'Do not simulate user messages, assistant turns, or tool results. Output only your next real tool call and wait for execution.' : '')
|
|
392
|
+
|| (skippedRejectedWork ? 'Previously rejected coding calls have not executed. Issue the remaining tool calls, or report a blocked outcome; do not declare them complete.' : '')
|
|
393
|
+
|| (mixedBatch ? 'Send OpenChamber operation tools one at a time, without other calls in the same response.' : '')
|
|
394
|
+
|| (malformedFinal ? 'The final declaration must be a single valid openchamber-final JSON fence with status and text, without other content.' : '')
|
|
395
|
+
|| (requiresFinal && parsed.toolCalls.length === 0 && finalText === null ? 'A workspace response needs an actual tool call or an explicit final declaration.' : '');
|
|
396
|
+
if (!protocolError) break;
|
|
397
|
+
if (attempt >= 2) throw new Error('Assistant response protocol failed after two corrections; the task was not confirmed complete.');
|
|
398
|
+
const rejected = parsed.toolCalls.filter((call) => isPiCodingToolName(call.name));
|
|
399
|
+
if (rejected.length > 0) rejectedCodingCalls = rejected.map(({ name, arguments: args }) => ({ name, arguments: args }));
|
|
400
|
+
// Never replay rejected prose: it may contain fabricated user/tool results.
|
|
401
|
+
messages.push(
|
|
402
|
+
{ 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.'}` },
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta);
|
|
406
|
+
if (parsed.toolCalls.length > 0) {
|
|
407
|
+
const toolCalls = parsed.toolCalls.map((call) => ({
|
|
318
408
|
type: 'toolCall',
|
|
319
|
-
id: `call_${Date.now().toString(36)}`,
|
|
320
|
-
name:
|
|
321
|
-
arguments:
|
|
322
|
-
};
|
|
409
|
+
id: `call_${Date.now().toString(36)}_${++callSequence}`,
|
|
410
|
+
name: call.name,
|
|
411
|
+
arguments: call.arguments,
|
|
412
|
+
}));
|
|
323
413
|
const spoken = isContactSpokenPreamble(parsed.chatText) ? parsed.chatText.trim() : '';
|
|
324
414
|
const content = spoken
|
|
325
|
-
? [{ type: 'text', text: spoken },
|
|
326
|
-
:
|
|
415
|
+
? [{ type: 'text', text: spoken }, ...toolCalls]
|
|
416
|
+
: toolCalls;
|
|
327
417
|
const partial = {
|
|
328
418
|
...assistantMessage(model, spoken, 'toolUse'),
|
|
329
419
|
content,
|
|
@@ -335,15 +425,17 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
335
425
|
stream.push({ type: 'text_delta', contentIndex: 0, delta: spoken, partial });
|
|
336
426
|
stream.push({ type: 'text_end', contentIndex: 0, content: spoken, partial });
|
|
337
427
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
428
|
+
toolCalls.forEach((toolCall, index) => {
|
|
429
|
+
const toolIndex = index + (spoken ? 1 : 0);
|
|
430
|
+
stream.push({ type: 'toolcall_start', contentIndex: toolIndex, partial });
|
|
431
|
+
stream.push({ type: 'toolcall_delta', contentIndex: toolIndex, delta: JSON.stringify(toolCall.arguments), partial });
|
|
432
|
+
stream.push({ type: 'toolcall_end', contentIndex: toolIndex, toolCall, partial });
|
|
433
|
+
});
|
|
342
434
|
stream.push({ type: 'done', reason: 'toolUse', message: partial });
|
|
343
435
|
stream.end(partial);
|
|
344
436
|
return;
|
|
345
437
|
}
|
|
346
|
-
const chatText =
|
|
438
|
+
const chatText = finalText ?? parsed.chatText;
|
|
347
439
|
const replyBubbles = splitContactBubbles(chatText);
|
|
348
440
|
if (bubbleGapMs > 0 && replyBubbles.length > 1) {
|
|
349
441
|
for (let index = 0; index < replyBubbles.length; index += 1) {
|
|
@@ -363,7 +455,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
363
455
|
stream.push({ type: 'done', reason: 'stop', message: partial });
|
|
364
456
|
stream.end(partial);
|
|
365
457
|
} catch (error) {
|
|
366
|
-
const failed = assistantMessage(model, '', 'error', error?.message || 'upstream_error');
|
|
458
|
+
const failed = assistantMessage(model, '', signal?.aborted ? 'aborted' : 'error', error?.message || 'upstream_error');
|
|
367
459
|
stream.push({ type: 'error', reason: 'error', error: failed });
|
|
368
460
|
stream.end(failed);
|
|
369
461
|
}
|
|
@@ -427,6 +519,10 @@ const extractToolResultText = (messages) => {
|
|
|
427
519
|
|
|
428
520
|
/** Tool confirms that are themselves the user-facing bubble (no card). */
|
|
429
521
|
const TOOL_TEXT_BUBBLE_TOOLS = new Set([
|
|
522
|
+
STOP_SESSION_TOOL_NAME,
|
|
523
|
+
STEER_SESSION_TOOL_NAME,
|
|
524
|
+
ARCHIVE_SESSION_TOOL_NAME,
|
|
525
|
+
DELETE_SESSION_TOOL_NAME,
|
|
430
526
|
NEW_CONVERSATION_TOOL_NAME,
|
|
431
527
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
432
528
|
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
@@ -436,6 +532,8 @@ const TOOL_TEXT_BUBBLE_TOOLS = new Set([
|
|
|
436
532
|
/** Card / side-effect tools: never paint English toolText into the transcript. */
|
|
437
533
|
const CARD_SIDE_EFFECT_TOOLS = new Set([
|
|
438
534
|
ASSIGN_SESSION_TOOL_NAME,
|
|
535
|
+
WATCH_SESSION_TOOL_NAME,
|
|
536
|
+
STOP_SESSION_TOOL_NAME,
|
|
439
537
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
440
538
|
SCHEDULE_TASK_TOOL_NAME,
|
|
441
539
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
@@ -504,34 +602,45 @@ export async function runContactTurn({
|
|
|
504
602
|
tools = [],
|
|
505
603
|
projects = [],
|
|
506
604
|
connectedModels = [],
|
|
605
|
+
modelPreferences = null,
|
|
606
|
+
modelCatalogAvailable = true,
|
|
507
607
|
onTextDelta = null,
|
|
508
608
|
onBubbleDelta = null,
|
|
509
609
|
globalEventHub = null,
|
|
510
610
|
skillHomeDir,
|
|
611
|
+
language = '',
|
|
511
612
|
AgentImpl = Agent,
|
|
613
|
+
signal = null,
|
|
614
|
+
readOnly = false,
|
|
512
615
|
}) {
|
|
513
616
|
const providerID = assistant.providerID;
|
|
514
617
|
const modelID = assistant.modelID;
|
|
515
618
|
const cwd = resolveAssistantCwd(assistant);
|
|
516
619
|
const contactTools = Array.isArray(tools)
|
|
517
|
-
? tools.filter((tool) => tool && typeof tool.name === 'string' && !isPiCodingToolName(tool.name))
|
|
620
|
+
? 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
621
|
: [];
|
|
519
622
|
let runtime = null;
|
|
623
|
+
let removeAbortListener = null;
|
|
520
624
|
try {
|
|
521
|
-
|
|
625
|
+
signal?.throwIfAborted();
|
|
626
|
+
if (cwd && !readOnly) runtime = await createPiCodingRuntime(cwd, { homeDir: skillHomeDir });
|
|
522
627
|
const codingTools = Array.isArray(runtime?.tools) ? runtime.tools : [];
|
|
523
628
|
const systemPrompt = [
|
|
524
629
|
CONTACT_SYSTEM_PROMPT,
|
|
630
|
+
'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.',
|
|
631
|
+
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
632
|
formatPiCodingPrompt({
|
|
526
633
|
cwd: runtime?.cwd,
|
|
527
634
|
skillsPrompt: runtime?.skillsPrompt,
|
|
528
635
|
tools: codingTools,
|
|
529
636
|
}),
|
|
530
637
|
formatRegisteredProjectsPrompt(projects),
|
|
531
|
-
formatConnectedModelsPrompt(connectedModels),
|
|
638
|
+
formatConnectedModelsPrompt(connectedModels, { preferences: modelPreferences, catalogAvailable: modelCatalogAvailable }),
|
|
532
639
|
formatContactToolsPrompt(contactTools),
|
|
533
640
|
assistant.defaultPrompt,
|
|
534
|
-
|
|
641
|
+
codingTools.length > 0 ? WORKSPACE_RESPONSE_PROTOCOL : '',
|
|
642
|
+
].filter((value) => typeof value === 'string' && value.trim()).join('\n\n')
|
|
643
|
+
.replaceAll('{{LANGUAGE}}', resolveContactLanguage(language));
|
|
535
644
|
const model = createContactModel(providerID, modelID);
|
|
536
645
|
// streamFn receives the model; completions needs providerID/modelID.
|
|
537
646
|
model.name = `${providerID}/${modelID}`;
|
|
@@ -554,6 +663,7 @@ export async function runContactTurn({
|
|
|
554
663
|
: [];
|
|
555
664
|
|
|
556
665
|
const agent = new AgentImpl({
|
|
666
|
+
toolExecution: 'sequential',
|
|
557
667
|
initialState: {
|
|
558
668
|
systemPrompt,
|
|
559
669
|
model,
|
|
@@ -567,10 +677,16 @@ export async function runContactTurn({
|
|
|
567
677
|
onBubbleDelta,
|
|
568
678
|
globalEventHub,
|
|
569
679
|
bubbleGapMs: 280,
|
|
680
|
+
signal,
|
|
570
681
|
}),
|
|
571
682
|
});
|
|
572
683
|
|
|
684
|
+
const onAbort = () => agent.abort();
|
|
685
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
686
|
+
removeAbortListener = () => signal?.removeEventListener('abort', onAbort);
|
|
687
|
+
signal?.throwIfAborted();
|
|
573
688
|
await agent.prompt(userText);
|
|
689
|
+
signal?.throwIfAborted();
|
|
574
690
|
if (agent.state.errorMessage) {
|
|
575
691
|
const error = new Error(agent.state.errorMessage);
|
|
576
692
|
error.code = 'upstream_error';
|
|
@@ -583,7 +699,9 @@ export async function runContactTurn({
|
|
|
583
699
|
let retried = false;
|
|
584
700
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
585
701
|
retried = true;
|
|
702
|
+
signal?.throwIfAborted();
|
|
586
703
|
await agent.prompt(MISSED_FENCE_RETRY_USER_TEXT);
|
|
704
|
+
signal?.throwIfAborted();
|
|
587
705
|
if (agent.state.errorMessage) {
|
|
588
706
|
const error = new Error(agent.state.errorMessage);
|
|
589
707
|
error.code = 'upstream_error';
|
|
@@ -613,7 +731,7 @@ export async function runContactTurn({
|
|
|
613
731
|
};
|
|
614
732
|
}
|
|
615
733
|
const outcome = extractContactTurnOutcome(agent.state.messages, retried);
|
|
616
|
-
const text =
|
|
734
|
+
const text = outcome.text;
|
|
617
735
|
if (contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
618
736
|
const historyCleared = contactTurnClearedChatHistory(agent.state.messages);
|
|
619
737
|
const preferredConfirm = historyCleared
|
|
@@ -644,6 +762,7 @@ export async function runContactTurn({
|
|
|
644
762
|
tools: [...agent.state.tools],
|
|
645
763
|
};
|
|
646
764
|
} finally {
|
|
765
|
+
removeAbortListener?.();
|
|
647
766
|
try {
|
|
648
767
|
await runtime?.close?.();
|
|
649
768
|
} catch {
|