@openchambery/web 1.19.13-beta.3 → 1.19.13-beta.5
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-B5MiX004.js} +1 -1
- package/dist/assets/AssistantView-B6kZZdri.js +2 -0
- package/dist/assets/{DiagramView-W0IHFk-1.js → DiagramView-C40ituM2.js} +1 -1
- package/dist/assets/{FadeInOnReveal-moBbL8Ll.js → FadeInOnReveal-UGwClbL_.js} +1 -1
- package/dist/assets/{Icon-CkzzhaLr.js → Icon-ZAKaNHiG.js} +1 -1
- package/dist/assets/{MarkdownRenderer-v7n_D6nL.js → MarkdownRenderer-mcY597G0.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-Buz5LZsa.js → MarkdownRendererImpl-YsTrEeUY.js} +6 -3
- package/dist/assets/MarkstreamRendererImpl-kNx_G9Sd.js +3 -0
- package/dist/assets/MobileDetailNavigation-Uwzh6TWj.js +1 -0
- package/dist/assets/MobileShareBridge-D3oSiSC6.js +4 -0
- package/dist/assets/MobileSurface-B-SWjAj7.js +2 -0
- package/dist/assets/{MultiRunWindow-yWIHgr89.js → MultiRunWindow-LgdUonfa.js} +1 -1
- package/dist/assets/{OnboardingScreen-j0iCyH7f.js → OnboardingScreen-DHKq6y3W.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CDC5B3RH.js → OverlayScrollbar-Bahg_P3r.js} +1 -1
- package/dist/assets/{SettingsView-C3BbLpHT.js → SettingsView-iUiWTKbM.js} +8 -8
- package/dist/assets/{SettingsWindow-b0JWdygM.js → SettingsWindow-DCDEAC1n.js} +1 -1
- package/dist/assets/{TerminalView-C74IvmOt.js → TerminalView-CdZi_UXQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BORu8CtR.js → ToolOutputDialog-fZq6UHvy.js} +1 -1
- package/dist/assets/{appThemeRegistry-DOwhB93n.js → appThemeRegistry-DyFotcOM.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-U4ML8hxt.js} +1 -1
- package/dist/assets/{es-CRjOwvYM.js → es-BtC17YQH.js} +1 -1
- package/dist/assets/fileMentionTouchSelection-DSKcxgU_.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-D1mvv14J.js → gitApi-BUubBKri.js} +3 -3
- package/dist/assets/{input-CI730njs.js → input-Cd_1HyQv.js} +1 -1
- package/dist/assets/insertionBoundaries-DeGd-RP9.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-C6EluFKk.js +169 -0
- package/dist/assets/{main-phpzFgMN.js → main-iFGhn-zU.js} +2 -2
- package/dist/assets/{markdownHeightCache-DEjGKLv8.js → markdownHeightCache-BL8CUQ0M.js} +1 -1
- package/dist/assets/miniChat-Bs_H98PB.js +2 -0
- package/dist/assets/mobile-HfLUO-bG.js +2 -0
- package/dist/assets/{multirun-CYuxJPBX.js → multirun-BABuRg1O.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-Cir66uF3.js → projectMeta-WcxDg0DZ.js} +2 -2
- package/dist/assets/{pt-BR-CZQFJllP.js → pt-BR-DSejAksY.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BzYqqr9n.js → renderElectronMiniChatApp-Cp-dVXj5.js} +2 -2
- package/dist/assets/renderMobileApp-B6rb6wuc.js +28 -0
- package/dist/assets/{runtimeConfig-BXf9GQ0Q.js → runtimeConfig-88DzHVMT.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CEbKea6x.js +147 -0
- package/dist/assets/sessionChangeSummary-C9071lsK.js +2 -0
- package/dist/assets/sessionLookup-CEw3tCz1.js +1 -0
- package/dist/assets/{sortable-tabs-strip-Dom6Sthu.js → sortable-tabs-strip-eL2whsf8.js} +1 -1
- package/dist/assets/src-B_DG0xy5.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-NVyoI_Ei.js → useAppFontEffects-Ca0mASq5.js} +43 -44
- package/dist/assets/{useDesktopWindowControlsLayout-CP3NmMYG.js → useDesktopWindowControlsLayout-B3KJYYwp.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-AUoqSyOJ.js → useEffectiveDirectory-B4l9snxd.js} +1 -1
- package/dist/assets/{useFontPreferences-CB-cGo3I.js → useFontPreferences-DCaY8NbU.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CU2UwmN3.js +413 -0
- package/dist/assets/{useSessionAutoCleanup-ClvQQPqQ.js → useSessionAutoCleanup-B9El9yjX.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DOk4cAxv.js → useWorkerHighlightedLines-PPhFeDFI.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 +104 -18
- package/server/lib/assistants/assign.js +173 -13
- package/server/lib/assistants/assign.test.js +139 -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 +514 -52
- package/server/lib/assistants/contact-tools.test.js +390 -7
- package/server/lib/assistants/contracts.js +31 -0
- package/server/lib/assistants/harness.js +224 -54
- package/server/lib/assistants/harness.test.js +372 -12
- package/server/lib/assistants/routes.js +3 -0
- package/server/lib/assistants/service.js +1079 -38
- package/server/lib/assistants/service.test.js +1235 -26
- 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-DOZPK9oh.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-D4nUXYpP.js +0 -3
- package/dist/assets/MobileDetailNavigation-Bfs4CTUh.js +0 -1
- package/dist/assets/MobileShareBridge-DtwVLkBL.js +0 -4
- package/dist/assets/MobileSurface-BmYYdY3g.js +0 -2
- package/dist/assets/insertionBoundaries-BBn8YW82.js +0 -4
- package/dist/assets/main-Cd7NwXx5.js +0 -169
- package/dist/assets/miniChat-D6UYe50H.js +0 -2
- package/dist/assets/mobile-w2Ed9I5O.js +0 -2
- package/dist/assets/platform-DzsGqlGW.js +0 -2
- package/dist/assets/renderMobileApp-BvIE86_O.js +0 -28
- package/dist/assets/runtimeEndpointReset-lF5S7U4I.js +0 -147
- package/dist/assets/sessionChangeSummary-DQDZ5HW-.js +0 -1
- package/dist/assets/sessionLookup-CNRDupYq.js +0 -1
- package/dist/assets/src-AA-5nXsF.css +0 -2
- package/dist/assets/useMobileNavigationStore-DANrWekj.js +0 -413
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { Agent } from '@earendil-works/pi-agent-core';
|
|
2
2
|
import { isContactSpokenPreamble, splitContactBubbles } from './bubbles.js';
|
|
3
3
|
import {
|
|
4
|
+
ASSIGN_SESSION_TOOL_NAME,
|
|
4
5
|
CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
6
|
+
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
5
7
|
confirmBubbleAfterContactReset,
|
|
6
8
|
contactTurnClearedChatHistory,
|
|
7
9
|
contactTurnHasSuccessfulReset,
|
|
8
10
|
contactTurnHasToolResult,
|
|
11
|
+
CREATE_ASSISTANT_TOOL_NAME,
|
|
9
12
|
detectRequestedContactTools,
|
|
10
13
|
extractContactCardsFromMessages,
|
|
11
14
|
formatContactToolsPrompt,
|
|
12
15
|
formatConnectedModelsPrompt,
|
|
13
16
|
formatRegisteredProjectsPrompt,
|
|
17
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
18
|
+
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
14
19
|
MISSED_FENCE_RETRY_USER_TEXT,
|
|
15
20
|
MISSED_TOOL_FAILURE_BUBBLE,
|
|
16
21
|
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
22
|
+
NEW_CONVERSATION_TOOL_NAME,
|
|
17
23
|
parseContactToolCalls,
|
|
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,
|
|
18
29
|
stripContactToolFences,
|
|
30
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
31
|
+
WATCH_SESSION_TOOL_NAME,
|
|
19
32
|
} from './contact-tools.js';
|
|
20
33
|
import {
|
|
21
34
|
createPiCodingRuntime,
|
|
@@ -71,19 +84,72 @@ function createAssistantMessageEventStream() {
|
|
|
71
84
|
};
|
|
72
85
|
}
|
|
73
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
|
+
|
|
74
116
|
export const CONTACT_SYSTEM_PROMPT = [
|
|
75
117
|
"You are OpenChamber's in-app assistant — a personable contact who can also work in the user's configured project directory.",
|
|
76
118
|
'Reply in short chat bubbles: a few sentences each, separated by a blank line.',
|
|
77
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.',
|
|
78
121
|
'Never write chain-of-thought, plans, tool names, or English narration of what you will do. The user never sees thinking.',
|
|
79
122
|
'Do not expose tool traces, Activity, or editor actions.',
|
|
80
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.',
|
|
81
|
-
'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 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.',
|
|
82
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.',
|
|
83
|
-
'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.',
|
|
84
|
-
'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.',
|
|
85
128
|
].join(' ');
|
|
86
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
|
+
|
|
87
153
|
const emptyUsage = () => ({
|
|
88
154
|
input: 0,
|
|
89
155
|
output: 0,
|
|
@@ -229,7 +295,9 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
229
295
|
onBubbleDelta = null,
|
|
230
296
|
globalEventHub = null,
|
|
231
297
|
bubbleGapMs = 0,
|
|
298
|
+
signal = null,
|
|
232
299
|
} = {}) {
|
|
300
|
+
let callSequence = 0;
|
|
233
301
|
return (model, context) => {
|
|
234
302
|
const stream = createAssistantMessageEventStream();
|
|
235
303
|
const run = async () => {
|
|
@@ -249,10 +317,13 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
249
317
|
}];
|
|
250
318
|
}
|
|
251
319
|
if (message.role === 'assistant') {
|
|
252
|
-
const content = (message.content || [])
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.
|
|
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');
|
|
256
327
|
return content ? [{ role: 'assistant', content }] : [];
|
|
257
328
|
}
|
|
258
329
|
if (message.role === 'toolResult') {
|
|
@@ -260,7 +331,6 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
260
331
|
.filter((part) => part?.type === 'text')
|
|
261
332
|
.map((part) => part.text)
|
|
262
333
|
.join('');
|
|
263
|
-
if (!content) return [];
|
|
264
334
|
const toolName = typeof message.toolName === 'string' && message.toolName.trim()
|
|
265
335
|
? message.toolName.trim()
|
|
266
336
|
: 'result';
|
|
@@ -270,7 +340,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
270
340
|
const label = callId
|
|
271
341
|
? `OpenChamber tool result name=${toolName} call=${callId}`
|
|
272
342
|
: `OpenChamber tool result name=${toolName}`;
|
|
273
|
-
return [{ role: 'user', content: `${label}: ${content}` }];
|
|
343
|
+
return [{ role: 'user', content: `${label}: ${JSON.stringify({ isError: message.isError === true, content })}` }];
|
|
274
344
|
}
|
|
275
345
|
return [];
|
|
276
346
|
});
|
|
@@ -282,40 +352,68 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
282
352
|
if (lastUser && fallbackFiles.length > 0 && !Array.isArray(lastUser.parts)) {
|
|
283
353
|
lastUser.parts = fallbackFiles;
|
|
284
354
|
}
|
|
285
|
-
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta);
|
|
286
|
-
const result = await createChatCompletion({
|
|
287
|
-
body: {
|
|
288
|
-
model: `${model.provider === 'openchamber' ? '' : `${model.provider}/`}${model.id}`.replace(/^\//, '') || model.name,
|
|
289
|
-
providerID: typeof model.name === 'string' && model.name.includes('/')
|
|
290
|
-
? model.name.split('/')[0]
|
|
291
|
-
: undefined,
|
|
292
|
-
modelID: model.id,
|
|
293
|
-
messages,
|
|
294
|
-
},
|
|
295
|
-
onTextDelta: (delta) => {
|
|
296
|
-
if (typeof delta !== 'string' || !delta) return;
|
|
297
|
-
// Do not live-paint raw tokens: they are often chain-of-thought
|
|
298
|
-
// and would appear then vanish after parse/persist.
|
|
299
|
-
if (typeof onTextDelta === 'function') onTextDelta(delta);
|
|
300
|
-
},
|
|
301
|
-
globalEventHub,
|
|
302
|
-
});
|
|
303
|
-
const text = result?.completion?.choices?.[0]?.message?.content
|
|
304
|
-
?? result?.text
|
|
305
|
-
?? '';
|
|
306
355
|
const allowedNames = (context.tools || []).map((tool) => tool?.name).filter(Boolean);
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
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) => ({
|
|
310
408
|
type: 'toolCall',
|
|
311
|
-
id: `call_${Date.now().toString(36)}`,
|
|
312
|
-
name:
|
|
313
|
-
arguments:
|
|
314
|
-
};
|
|
409
|
+
id: `call_${Date.now().toString(36)}_${++callSequence}`,
|
|
410
|
+
name: call.name,
|
|
411
|
+
arguments: call.arguments,
|
|
412
|
+
}));
|
|
315
413
|
const spoken = isContactSpokenPreamble(parsed.chatText) ? parsed.chatText.trim() : '';
|
|
316
414
|
const content = spoken
|
|
317
|
-
? [{ type: 'text', text: spoken },
|
|
318
|
-
:
|
|
415
|
+
? [{ type: 'text', text: spoken }, ...toolCalls]
|
|
416
|
+
: toolCalls;
|
|
319
417
|
const partial = {
|
|
320
418
|
...assistantMessage(model, spoken, 'toolUse'),
|
|
321
419
|
content,
|
|
@@ -327,15 +425,17 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
327
425
|
stream.push({ type: 'text_delta', contentIndex: 0, delta: spoken, partial });
|
|
328
426
|
stream.push({ type: 'text_end', contentIndex: 0, content: spoken, partial });
|
|
329
427
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
});
|
|
334
434
|
stream.push({ type: 'done', reason: 'toolUse', message: partial });
|
|
335
435
|
stream.end(partial);
|
|
336
436
|
return;
|
|
337
437
|
}
|
|
338
|
-
const chatText =
|
|
438
|
+
const chatText = finalText ?? parsed.chatText;
|
|
339
439
|
const replyBubbles = splitContactBubbles(chatText);
|
|
340
440
|
if (bubbleGapMs > 0 && replyBubbles.length > 1) {
|
|
341
441
|
for (let index = 0; index < replyBubbles.length; index += 1) {
|
|
@@ -355,7 +455,7 @@ export function createContactStreamFn(createChatCompletion, {
|
|
|
355
455
|
stream.push({ type: 'done', reason: 'stop', message: partial });
|
|
356
456
|
stream.end(partial);
|
|
357
457
|
} catch (error) {
|
|
358
|
-
const failed = assistantMessage(model, '', 'error', error?.message || 'upstream_error');
|
|
458
|
+
const failed = assistantMessage(model, '', signal?.aborted ? 'aborted' : 'error', error?.message || 'upstream_error');
|
|
359
459
|
stream.push({ type: 'error', reason: 'error', error: failed });
|
|
360
460
|
stream.end(failed);
|
|
361
461
|
}
|
|
@@ -417,6 +517,28 @@ const extractToolResultText = (messages) => {
|
|
|
417
517
|
return '';
|
|
418
518
|
};
|
|
419
519
|
|
|
520
|
+
/** Tool confirms that are themselves the user-facing bubble (no card). */
|
|
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,
|
|
526
|
+
NEW_CONVERSATION_TOOL_NAME,
|
|
527
|
+
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
528
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
529
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
530
|
+
]);
|
|
531
|
+
|
|
532
|
+
/** Card / side-effect tools: never paint English toolText into the transcript. */
|
|
533
|
+
const CARD_SIDE_EFFECT_TOOLS = new Set([
|
|
534
|
+
ASSIGN_SESSION_TOOL_NAME,
|
|
535
|
+
WATCH_SESSION_TOOL_NAME,
|
|
536
|
+
STOP_SESSION_TOOL_NAME,
|
|
537
|
+
CREATE_ASSISTANT_TOOL_NAME,
|
|
538
|
+
SCHEDULE_TASK_TOOL_NAME,
|
|
539
|
+
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
540
|
+
]);
|
|
541
|
+
|
|
420
542
|
const extractContactTurnOutcome = (messages, retried) => {
|
|
421
543
|
const list = Array.isArray(messages) ? messages : [];
|
|
422
544
|
let start = 0;
|
|
@@ -440,9 +562,22 @@ const extractContactTurnOutcome = (messages, retried) => {
|
|
|
440
562
|
const coding = isPiCodingToolName(lastToolName);
|
|
441
563
|
const toolText = extractToolResultText(slice);
|
|
442
564
|
const assistant = extractAssistantText(slice);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
565
|
+
let confirm = '';
|
|
566
|
+
if (hasTool && !coding) {
|
|
567
|
+
if (TOOL_TEXT_BUBBLE_TOOLS.has(lastToolName)) {
|
|
568
|
+
// Confirm-only tools: toolText is the user bubble.
|
|
569
|
+
confirm = toolText || assistant;
|
|
570
|
+
} else if (CARD_SIDE_EFFECT_TOOLS.has(lastToolName)) {
|
|
571
|
+
// Spoken preamble only. Card is enough when present; never English toolText.
|
|
572
|
+
// Post-tool assistant text (terminate:false tools) may still surface when no spoken.
|
|
573
|
+
confirm = spoken ? '' : (assistant || '');
|
|
574
|
+
} else {
|
|
575
|
+
// list_projects / list_sessions and other app tools keep toolText.
|
|
576
|
+
confirm = toolText || assistant;
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
confirm = assistant || (coding ? toolText : '');
|
|
580
|
+
}
|
|
446
581
|
const parts = [];
|
|
447
582
|
if (spoken) parts.push(spoken);
|
|
448
583
|
if (confirm && confirm !== spoken) parts.push(confirm);
|
|
@@ -467,34 +602,45 @@ export async function runContactTurn({
|
|
|
467
602
|
tools = [],
|
|
468
603
|
projects = [],
|
|
469
604
|
connectedModels = [],
|
|
605
|
+
modelPreferences = null,
|
|
606
|
+
modelCatalogAvailable = true,
|
|
470
607
|
onTextDelta = null,
|
|
471
608
|
onBubbleDelta = null,
|
|
472
609
|
globalEventHub = null,
|
|
473
610
|
skillHomeDir,
|
|
611
|
+
language = '',
|
|
474
612
|
AgentImpl = Agent,
|
|
613
|
+
signal = null,
|
|
614
|
+
readOnly = false,
|
|
475
615
|
}) {
|
|
476
616
|
const providerID = assistant.providerID;
|
|
477
617
|
const modelID = assistant.modelID;
|
|
478
618
|
const cwd = resolveAssistantCwd(assistant);
|
|
479
619
|
const contactTools = Array.isArray(tools)
|
|
480
|
-
? 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)))
|
|
481
621
|
: [];
|
|
482
622
|
let runtime = null;
|
|
623
|
+
let removeAbortListener = null;
|
|
483
624
|
try {
|
|
484
|
-
|
|
625
|
+
signal?.throwIfAborted();
|
|
626
|
+
if (cwd && !readOnly) runtime = await createPiCodingRuntime(cwd, { homeDir: skillHomeDir });
|
|
485
627
|
const codingTools = Array.isArray(runtime?.tools) ? runtime.tools : [];
|
|
486
628
|
const systemPrompt = [
|
|
487
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.' : '',
|
|
488
632
|
formatPiCodingPrompt({
|
|
489
633
|
cwd: runtime?.cwd,
|
|
490
634
|
skillsPrompt: runtime?.skillsPrompt,
|
|
491
635
|
tools: codingTools,
|
|
492
636
|
}),
|
|
493
637
|
formatRegisteredProjectsPrompt(projects),
|
|
494
|
-
formatConnectedModelsPrompt(connectedModels),
|
|
638
|
+
formatConnectedModelsPrompt(connectedModels, { preferences: modelPreferences, catalogAvailable: modelCatalogAvailable }),
|
|
495
639
|
formatContactToolsPrompt(contactTools),
|
|
496
640
|
assistant.defaultPrompt,
|
|
497
|
-
|
|
641
|
+
codingTools.length > 0 ? WORKSPACE_RESPONSE_PROTOCOL : '',
|
|
642
|
+
].filter((value) => typeof value === 'string' && value.trim()).join('\n\n')
|
|
643
|
+
.replaceAll('{{LANGUAGE}}', resolveContactLanguage(language));
|
|
498
644
|
const model = createContactModel(providerID, modelID);
|
|
499
645
|
// streamFn receives the model; completions needs providerID/modelID.
|
|
500
646
|
model.name = `${providerID}/${modelID}`;
|
|
@@ -517,6 +663,7 @@ export async function runContactTurn({
|
|
|
517
663
|
: [];
|
|
518
664
|
|
|
519
665
|
const agent = new AgentImpl({
|
|
666
|
+
toolExecution: 'sequential',
|
|
520
667
|
initialState: {
|
|
521
668
|
systemPrompt,
|
|
522
669
|
model,
|
|
@@ -530,10 +677,16 @@ export async function runContactTurn({
|
|
|
530
677
|
onBubbleDelta,
|
|
531
678
|
globalEventHub,
|
|
532
679
|
bubbleGapMs: 280,
|
|
680
|
+
signal,
|
|
533
681
|
}),
|
|
534
682
|
});
|
|
535
683
|
|
|
684
|
+
const onAbort = () => agent.abort();
|
|
685
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
686
|
+
removeAbortListener = () => signal?.removeEventListener('abort', onAbort);
|
|
687
|
+
signal?.throwIfAborted();
|
|
536
688
|
await agent.prompt(userText);
|
|
689
|
+
signal?.throwIfAborted();
|
|
537
690
|
if (agent.state.errorMessage) {
|
|
538
691
|
const error = new Error(agent.state.errorMessage);
|
|
539
692
|
error.code = 'upstream_error';
|
|
@@ -546,7 +699,9 @@ export async function runContactTurn({
|
|
|
546
699
|
let retried = false;
|
|
547
700
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
548
701
|
retried = true;
|
|
702
|
+
signal?.throwIfAborted();
|
|
549
703
|
await agent.prompt(MISSED_FENCE_RETRY_USER_TEXT);
|
|
704
|
+
signal?.throwIfAborted();
|
|
550
705
|
if (agent.state.errorMessage) {
|
|
551
706
|
const error = new Error(agent.state.errorMessage);
|
|
552
707
|
error.code = 'upstream_error';
|
|
@@ -554,6 +709,19 @@ export async function runContactTurn({
|
|
|
554
709
|
}
|
|
555
710
|
}
|
|
556
711
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
712
|
+
// Prefer any spoken/assistant text already in the turn over the English failure fallback.
|
|
713
|
+
const missed = extractContactTurnOutcome(agent.state.messages, retried);
|
|
714
|
+
const missedText = stripContactToolFences(missed.text).trim();
|
|
715
|
+
if (missedText) {
|
|
716
|
+
const missedBubbles = splitContactBubbles(missedText);
|
|
717
|
+
return {
|
|
718
|
+
text: missedText,
|
|
719
|
+
bubbles: missedBubbles.length > 0 ? missedBubbles : [missedText],
|
|
720
|
+
cards: [],
|
|
721
|
+
thinkingLevel: agent.state.thinkingLevel,
|
|
722
|
+
tools: [...agent.state.tools],
|
|
723
|
+
};
|
|
724
|
+
}
|
|
557
725
|
return {
|
|
558
726
|
text: MISSED_TOOL_FAILURE_BUBBLE,
|
|
559
727
|
bubbles: [MISSED_TOOL_FAILURE_BUBBLE],
|
|
@@ -563,7 +731,7 @@ export async function runContactTurn({
|
|
|
563
731
|
};
|
|
564
732
|
}
|
|
565
733
|
const outcome = extractContactTurnOutcome(agent.state.messages, retried);
|
|
566
|
-
const text =
|
|
734
|
+
const text = outcome.text;
|
|
567
735
|
if (contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
568
736
|
const historyCleared = contactTurnClearedChatHistory(agent.state.messages);
|
|
569
737
|
const preferredConfirm = historyCleared
|
|
@@ -580,7 +748,8 @@ export async function runContactTurn({
|
|
|
580
748
|
tools: [...agent.state.tools],
|
|
581
749
|
};
|
|
582
750
|
}
|
|
583
|
-
|
|
751
|
+
// Card tools / side-effect tools may finish with cards only (no English toolText bubble).
|
|
752
|
+
if (!text.trim() && outcome.cards.length === 0 && !outcome.hasTool) {
|
|
584
753
|
const error = new Error('Assistant returned no text');
|
|
585
754
|
error.code = 'upstream_error';
|
|
586
755
|
throw error;
|
|
@@ -593,6 +762,7 @@ export async function runContactTurn({
|
|
|
593
762
|
tools: [...agent.state.tools],
|
|
594
763
|
};
|
|
595
764
|
} finally {
|
|
765
|
+
removeAbortListener?.();
|
|
596
766
|
try {
|
|
597
767
|
await runtime?.close?.();
|
|
598
768
|
} catch {
|