@openchambery/web 1.19.7-beta.9 → 1.19.7
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-CX0a1rKE.js +1 -0
- package/dist/assets/AssistantView-nVF0kdFh.js +2 -0
- package/dist/assets/DiagramView-B2UtQ8mw.js +1 -0
- package/dist/assets/FadeInOnReveal-CK4asqVR.js +1 -0
- package/dist/assets/Icon-BYM02QyU.js +1 -0
- package/dist/assets/MarkdownLoadingSkeleton-DWs4Jg7b.js +3 -0
- package/dist/assets/MarkdownRenderer-DT_GN1Ex.js +2 -0
- package/dist/assets/MarkdownRendererImpl-Cslh4XN_.js +7 -0
- package/dist/assets/MarkstreamRendererImpl-CsNwy1T5.css +1 -0
- package/dist/assets/MarkstreamRendererImpl-Ymm12x6J.js +1 -0
- package/dist/assets/MobileDetailNavigation-DD29ENgU.js +1 -0
- package/dist/assets/MobileShareBridge-CrTe_QND.js +4 -0
- package/dist/assets/MobileSurface-DjxchpUC.js +2 -0
- package/dist/assets/{MultiRunWindow-Ohj6UbWj.js → MultiRunWindow-jG5dCEMH.js} +1 -1
- package/dist/assets/OnboardingScreen-C3-wzeSX.js +1 -0
- package/dist/assets/OverlayScrollbar-KUpvYHdv.js +2 -0
- package/dist/assets/SettingsView-UKlEewdq.js +25 -0
- package/dist/assets/{SettingsWindow-B4lzhAbO.js → SettingsWindow-DO1aAplT.js} +1 -1
- package/dist/assets/TerminalView-BG1UPLsz.js +8 -0
- package/dist/assets/ToolOutputDialog-CWr5eVTK.js +10 -0
- package/dist/assets/appThemeRegistry-BI5drVpC.js +1 -0
- package/dist/assets/{button-z0GOBtWi.js → button-C0cth9tl.js} +1 -1
- package/dist/assets/clipboard-DJ4nQEuY.js +1 -0
- package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-DFT9cE80.js} +1 -1
- package/dist/assets/context-menu-CAp74NfV.js +1 -0
- package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
- package/dist/assets/desktopRelayRestore-9EbBYQ9s.js +1 -0
- package/dist/assets/{diagram-CoEqmReY.js → diagram-BryzMuta.js} +1 -1
- package/dist/assets/{dialog-x2hirWBd.js → dialog-CgbV5ljg.js} +1 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
- package/dist/assets/{es-C8Sqjsia.js → es-BHqnSjoZ.js} +1 -1
- package/dist/assets/fileQueries-BY-9OWzp.js +1 -0
- package/dist/assets/fileReferenceActions-BJJtX_S5.js +1 -0
- package/dist/assets/{fr-p-Zwm_j2.js → fr-BfrlJY9i.js} +1 -1
- package/dist/assets/{gitApi-CtS4kWoo.js → gitApi-D9hFlE3T.js} +3 -3
- package/dist/assets/{input-BG3VWTIF.js → input-DFLpqjGR.js} +1 -1
- package/dist/assets/insertionBoundaries-D2L7D0Oe.js +4 -0
- package/dist/assets/{ja-B6REu1qP.js → ja-DP-7qt-x.js} +1 -1
- package/dist/assets/{ko-CJBNH9IZ.js → ko-E2B5vGQP.js} +1 -1
- package/dist/assets/main-BRedOs_V.js +169 -0
- package/dist/assets/main-DlXpJ0Vw.js +2 -0
- package/dist/assets/markdown-worker-BuveaH3O.js +1 -0
- package/dist/assets/markdownTheme-CzZm3riB.js +2 -0
- package/dist/assets/miniChat-CBLbFpfy.js +2 -0
- package/dist/assets/mobile-CoZsWdWk.js +2 -0
- package/dist/assets/multirun-BF-I0r2_.js +4 -0
- package/dist/assets/outsideFileGrants-DnplRxr-.js +1 -0
- package/dist/assets/path-utils-jGozTg9d.js +1 -0
- package/dist/assets/persistence-DdUac14I.js +3 -0
- package/dist/assets/{pl-B2cBI_7K.js → pl-r8X5gRM5.js} +1 -1
- package/dist/assets/platform-B4KKLnZ5.js +2 -0
- package/dist/assets/projectMeta-2t4L5_4a.js +3 -0
- package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-fl7O3iHy.js +2 -0
- package/dist/assets/renderMobileApp-CKYDwf4U.js +28 -0
- package/dist/assets/runtimeConfig-B4eb4ZiU.js +2 -0
- package/dist/assets/runtimeEndpointReset-BHuTYyzc.js +147 -0
- package/dist/assets/sessionChangeSummary-DOq8EN_H.js +1 -0
- package/dist/assets/sessionLookup-DNc0ZeR3.js +1 -0
- package/dist/assets/sortable-tabs-strip-C8_9kHqu.js +1 -0
- package/dist/assets/src-DasvfkEg.css +2 -0
- package/dist/assets/{store-CL3WP48T.js → store-BDFdnk6l.js} +2 -2
- package/dist/assets/terminalApi-947l7Ra3.js +1 -0
- package/dist/assets/textarea-Cgo_nWUU.js +1 -0
- package/dist/assets/toolHelpers-DEvVP9U0.js +2 -0
- package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BifLJ8o0.js} +1 -1
- package/dist/assets/{uk-C8YKAz0x.js → uk-Di0vl9V-.js} +1 -1
- package/dist/assets/url-Pk6tQ1EZ.js +1 -0
- package/dist/assets/{ChatContainer-CDExXwgn.js → useAppFontEffects-Dle8xs5h.js} +44 -39
- package/dist/assets/useAssistantUIStore-me6s63b2.js +1 -0
- package/dist/assets/useDesktopWindowControlsLayout-_QAwKcQn.js +1 -0
- package/dist/assets/useEffectiveDirectory-BSyuAgz6.js +1 -0
- package/dist/assets/useFontPreferences-BCOBlaAd.js +1 -0
- package/dist/assets/useMobileNavigationStore-f8xHJk7C.js +413 -0
- package/dist/assets/useSessionAutoCleanup-BHiXaW0J.js +1 -0
- package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
- package/dist/assets/useWorkerHighlightedLines-DuvXTSm_.js +7 -0
- package/dist/assets/utils-BiK3tBGM.js +1 -0
- package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
- package/dist/assets/{zh-CN-CCYwUnmB.js → zh-CN-coFCfeQE.js} +1 -1
- package/dist/assets/{zh-TW-D8Y6py08.js → zh-TW-oDRZ1-yb.js} +1 -1
- package/dist/index.html +9 -11
- package/dist/mini-chat.html +9 -11
- package/dist/mobile.html +15 -17
- package/package.json +2 -1
- package/server/index.js +2 -0
- package/server/lib/assistant-delivery-parts.js +3 -1
- package/server/lib/assistant-delivery-parts.test.js +11 -0
- package/server/lib/assistants/DOCUMENTATION.md +76 -3
- package/server/lib/assistants/assign.js +225 -0
- package/server/lib/assistants/assign.test.js +118 -0
- package/server/lib/assistants/bubbles.js +45 -0
- package/server/lib/assistants/bubbles.test.js +36 -0
- package/server/lib/assistants/cards.js +148 -0
- package/server/lib/assistants/cards.test.js +116 -0
- package/server/lib/assistants/contact-store.js +284 -0
- package/server/lib/assistants/contact-store.test.js +131 -0
- package/server/lib/assistants/contact-tools.js +761 -0
- package/server/lib/assistants/contact-tools.test.js +328 -0
- package/server/lib/assistants/contracts.js +20 -1
- package/server/lib/assistants/harness.js +551 -0
- package/server/lib/assistants/harness.test.js +607 -0
- package/server/lib/assistants/routes.js +44 -4
- package/server/lib/assistants/service.js +930 -17
- package/server/lib/assistants/service.test.js +1163 -45
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/directory-ws-bridge.js +11 -2
- package/server/lib/event-stream/global-ws-bridge.js +19 -3
- package/server/lib/event-stream/protocol.js +11 -1
- package/server/lib/event-stream/reasoning-projection.js +425 -0
- package/server/lib/event-stream/reasoning-projection.test.js +300 -0
- package/server/lib/event-stream/runtime.js +10 -0
- package/server/lib/event-stream/runtime.test.js +53 -0
- package/server/lib/llm/DOCUMENTATION.md +84 -0
- package/server/lib/llm/catalog.js +100 -0
- package/server/lib/llm/catalog.test.js +78 -0
- package/server/lib/llm/completions.js +151 -0
- package/server/lib/llm/completions.test.js +127 -0
- package/server/lib/llm/generate.js +570 -0
- package/server/lib/llm/generate.test.js +459 -0
- package/server/lib/llm/routes.js +47 -0
- package/server/lib/llm/temp-directory.js +37 -0
- package/server/lib/notifications/APNS.md +19 -13
- package/server/lib/notifications/DOCUMENTATION.md +4 -3
- package/server/lib/notifications/runtime.js +148 -37
- package/server/lib/notifications/runtime.test.js +100 -0
- package/server/lib/opencode/DOCUMENTATION.md +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +30 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
- package/server/lib/opencode/proxy.js +103 -9
- package/server/lib/opencode/proxy.test.js +177 -1
- package/server/lib/opencode/settings-runtime.js +2 -2
- package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
- package/server/lib/session-goal/DOCUMENTATION.md +8 -1
- package/server/lib/session-goal/runtime.js +20 -0
- package/server/lib/session-index/DOCUMENTATION.md +3 -1
- package/server/lib/session-index/service.js +10 -4
- package/server/lib/session-index/service.test.js +37 -0
- package/server/lib/session-title/runtime.js +1 -0
- package/server/lib/session-title/runtime.test.js +11 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
- package/server/lib/session-turn-pages/routes.js +22 -3
- package/server/lib/session-turn-pages/routes.test.js +43 -0
- package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
- package/server/lib/transcript-cache/routes.js +9 -2
- package/server/lib/transcript-cache/routes.test.js +32 -0
- package/dist/assets/AgentAvatar-Di8F4QJh.js +0 -1
- package/dist/assets/AssistantView-DSxVAAb_.js +0 -1
- package/dist/assets/ContextPanel-DgwEIdqR.js +0 -285
- package/dist/assets/DiagramView-BKQO61y8.js +0 -1
- package/dist/assets/FadeInOnReveal-CV2s6YN8.js +0 -1
- package/dist/assets/Icon-CLpn1mIz.js +0 -1
- package/dist/assets/MarkdownRendererImpl-CTgmghSh.js +0 -7
- package/dist/assets/MarkstreamRendererImpl-BKsc-avF.js +0 -1
- package/dist/assets/MarkstreamRendererImpl-CxeWpDCK.css +0 -1
- package/dist/assets/MobileDetailNavigation-BcA6rd8Q.js +0 -1
- package/dist/assets/MobileShareBridge-w6T2ZrsW.js +0 -4
- package/dist/assets/MobileSurface-CG1n7v0h.js +0 -2
- package/dist/assets/OnboardingScreen-D6rtE7rj.js +0 -1
- package/dist/assets/OverlayScrollbar-DlNZho3q.js +0 -2
- package/dist/assets/SettingsView-CiF3r83e.js +0 -25
- package/dist/assets/TerminalView-Ctx_Jt_b.js +0 -8
- package/dist/assets/ToolOutputDialog-Dv9_fUYl.js +0 -10
- package/dist/assets/appThemeRegistry-DnwUIQB9.js +0 -2
- package/dist/assets/clipboard-D35XSwID.js +0 -1
- package/dist/assets/desktop-CfLlmafK.js +0 -1
- package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
- package/dist/assets/desktopRelayRestore-8pvI_dSc.js +0 -1
- package/dist/assets/fileQueries-DGHZqZHc.js +0 -1
- package/dist/assets/insertionBoundaries-hK4mQj4W.js +0 -4
- package/dist/assets/main-BmbZFoO4.js +0 -29
- package/dist/assets/main-BwNhn8ml.js +0 -2
- package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
- package/dist/assets/markdownTheme-SPe4jOwX.js +0 -2
- package/dist/assets/miniChat-BN89siC3.js +0 -2
- package/dist/assets/mobile-DE9rVo5D.js +0 -2
- package/dist/assets/multirun-BiEuyDq3.js +0 -4
- package/dist/assets/persistence-w-AiX2i4.js +0 -3
- package/dist/assets/platform-4eCmsdol.js +0 -2
- package/dist/assets/projectMeta-DSLKcH1a.js +0 -3
- package/dist/assets/radio-HD5AB2Qf.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-f_UxpFjv.js +0 -2
- package/dist/assets/renderMobileApp-BAhCai6d.js +0 -28
- package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
- package/dist/assets/runtimeConfig-qG_oNBoa.js +0 -2
- package/dist/assets/runtimeEndpointReset-BDeKD3EM.js +0 -4
- package/dist/assets/shikiHighlight-BphLZnac.js +0 -1
- package/dist/assets/sortable-tabs-strip-CNDgyX0B.js +0 -1
- package/dist/assets/src-DZt1nWf5.css +0 -2
- package/dist/assets/terminalApi-CGtOtc8I.js +0 -1
- package/dist/assets/url-KeihbE0d.js +0 -1
- package/dist/assets/useAppFontEffects--0dwVTSm.js +0 -7
- package/dist/assets/useAssistantUIStore-I0VWrB5V.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-DsbeBLvv.js +0 -1
- package/dist/assets/useEffectiveDirectory-CHvvohcG.js +0 -1
- package/dist/assets/useFontPreferences-BLZ3y6mN.js +0 -1
- package/dist/assets/useMobileNavigationStore-peTDuC00.js +0 -412
- package/dist/assets/useSessionAutoCleanup-D_ZC9E5g.js +0 -1
- package/dist/assets/useUIStore-CNLmCoPU.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-B1YzuMYR.js +0 -7
- package/dist/assets/utils-CTL16t2f.js +0 -1
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { Agent } from '@earendil-works/pi-agent-core';
|
|
2
|
+
import { isContactSpokenPreamble, splitContactBubbles } from './bubbles.js';
|
|
3
|
+
import {
|
|
4
|
+
confirmBubbleAfterContactReset,
|
|
5
|
+
contactTurnHasSuccessfulReset,
|
|
6
|
+
contactTurnHasToolResult,
|
|
7
|
+
detectRequestedContactTools,
|
|
8
|
+
extractContactCardsFromMessages,
|
|
9
|
+
formatContactToolsPrompt,
|
|
10
|
+
formatRegisteredProjectsPrompt,
|
|
11
|
+
MISSED_FENCE_RETRY_USER_TEXT,
|
|
12
|
+
MISSED_TOOL_FAILURE_BUBBLE,
|
|
13
|
+
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
14
|
+
parseContactToolCalls,
|
|
15
|
+
stripContactToolFences,
|
|
16
|
+
} from './contact-tools.js';
|
|
17
|
+
|
|
18
|
+
function createAssistantMessageEventStream() {
|
|
19
|
+
const events = [];
|
|
20
|
+
let pending = null;
|
|
21
|
+
let done = false;
|
|
22
|
+
let resolveFinal = null;
|
|
23
|
+
const finalResult = new Promise((resolve) => { resolveFinal = resolve; });
|
|
24
|
+
const wake = () => {
|
|
25
|
+
pending?.();
|
|
26
|
+
pending = null;
|
|
27
|
+
};
|
|
28
|
+
const finish = (message) => {
|
|
29
|
+
if (resolveFinal) {
|
|
30
|
+
resolveFinal(message);
|
|
31
|
+
resolveFinal = null;
|
|
32
|
+
}
|
|
33
|
+
done = true;
|
|
34
|
+
wake();
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
push(event) {
|
|
38
|
+
events.push(event);
|
|
39
|
+
if (event?.type === 'done') finish(event.message);
|
|
40
|
+
else if (event?.type === 'error') finish(event.error);
|
|
41
|
+
else wake();
|
|
42
|
+
},
|
|
43
|
+
end(message) {
|
|
44
|
+
// Completion is already a typed event (`done` / `error`). Do not push
|
|
45
|
+
// the raw assistant message again — callers read events.at(-1).type.
|
|
46
|
+
finish(message);
|
|
47
|
+
},
|
|
48
|
+
result() {
|
|
49
|
+
return finalResult;
|
|
50
|
+
},
|
|
51
|
+
async *[Symbol.asyncIterator]() {
|
|
52
|
+
let index = 0;
|
|
53
|
+
while (true) {
|
|
54
|
+
while (index < events.length) {
|
|
55
|
+
yield events[index];
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
if (done) return;
|
|
59
|
+
await new Promise((resolve) => { pending = resolve; });
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const CONTACT_SYSTEM_PROMPT = [
|
|
66
|
+
"You are OpenChamber's in-app assistant — a personable contact, not a coding agent.",
|
|
67
|
+
'Reply in short chat bubbles: a few sentences each, separated by a blank line.',
|
|
68
|
+
'Talk like a person in the user\'s language. One short spoken bubble at a time — never a wall of paragraphs.',
|
|
69
|
+
'Never write chain-of-thought, plans, tool names, or English narration of what you will do. The user never sees thinking.',
|
|
70
|
+
'Do not expose tool traces, Activity, or editor actions.',
|
|
71
|
+
'Do not run bash, edit, read, or write.',
|
|
72
|
+
'Understand natural language in any language, including Chinese: 开新对话 means new_conversation, 找项目 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.',
|
|
73
|
+
'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.',
|
|
74
|
+
'To open coding work: match the project, optionally list_sessions for existing chats, then assign_session with projectPath or sessionID.',
|
|
75
|
+
'A reply without the tool call does nothing. Never say 已创建, created, scheduled, or opened unless the tool already returned success.',
|
|
76
|
+
'Assign coding work with assign_session so a real Chat session does the work.',
|
|
77
|
+
].join(' ');
|
|
78
|
+
|
|
79
|
+
const emptyUsage = () => ({
|
|
80
|
+
input: 0,
|
|
81
|
+
output: 0,
|
|
82
|
+
cacheRead: 0,
|
|
83
|
+
cacheWrite: 0,
|
|
84
|
+
totalTokens: 0,
|
|
85
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export function createContactModel(providerID, modelID) {
|
|
89
|
+
return {
|
|
90
|
+
id: modelID,
|
|
91
|
+
name: `${providerID}/${modelID}`,
|
|
92
|
+
api: 'openai-completions',
|
|
93
|
+
provider: 'openchamber',
|
|
94
|
+
baseUrl: 'http://openchamber.invalid/api/openchamber/llm',
|
|
95
|
+
reasoning: false,
|
|
96
|
+
input: ['text', 'image'],
|
|
97
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
98
|
+
contextWindow: 128_000,
|
|
99
|
+
maxTokens: 8_192,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const assistantMessage = (model, text, stopReason, errorMessage) => ({
|
|
104
|
+
role: 'assistant',
|
|
105
|
+
content: text ? [{ type: 'text', text }] : [],
|
|
106
|
+
api: model.api,
|
|
107
|
+
provider: model.provider,
|
|
108
|
+
model: model.id,
|
|
109
|
+
usage: emptyUsage(),
|
|
110
|
+
stopReason,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* streamFn for pi-agent-core. Calls OpenChamber completions (public HTTP stays
|
|
117
|
+
* non-streaming). In-process callers may pass onTextDelta/globalEventHub for
|
|
118
|
+
* the throwaway generate path. Raw tokens are never painted as contact bubbles
|
|
119
|
+
* (they are often chain-of-thought). User-facing bubbles emit only after parse:
|
|
120
|
+
* no-tool replies as stripped bubbles; tool calls stay silent until the tool
|
|
121
|
+
* confirm. Must not throw — encode failures on the event stream.
|
|
122
|
+
*/
|
|
123
|
+
const completionFileParts = (value) => (Array.isArray(value) ? value : [])
|
|
124
|
+
.filter((part) => part?.type === 'file' && typeof part.mime === 'string' && typeof part.url === 'string')
|
|
125
|
+
.map((part) => ({
|
|
126
|
+
type: 'file',
|
|
127
|
+
mime: part.mime,
|
|
128
|
+
url: part.url,
|
|
129
|
+
...(typeof part.filename === 'string' && part.filename.trim() ? { filename: part.filename.trim() } : {}),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
/** Fence start (openchamber-tool or generic ```) — stop live bubble deltas. */
|
|
133
|
+
const CONTACT_FENCE_START = /```/;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Incremental bubble stream from live token deltas.
|
|
137
|
+
* - done:false = token increment for the active bubble
|
|
138
|
+
* - done:true = that bubble is complete (split off or finalized)
|
|
139
|
+
* Stops when a markdown fence starts so tool JSON never leaks.
|
|
140
|
+
*/
|
|
141
|
+
export function createContactBubbleDeltaTracker(onBubbleDelta) {
|
|
142
|
+
const emit = typeof onBubbleDelta === 'function' ? onBubbleDelta : null;
|
|
143
|
+
let accumulated = '';
|
|
144
|
+
let completedCount = 0;
|
|
145
|
+
let activeEmitted = '';
|
|
146
|
+
let stopped = false;
|
|
147
|
+
let emittedAny = false;
|
|
148
|
+
|
|
149
|
+
const push = (index, delta, done) => {
|
|
150
|
+
if (!emit || (typeof delta === 'string' && delta.length === 0 && !done)) return;
|
|
151
|
+
emittedAny = true;
|
|
152
|
+
emit(index, delta, done);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const applyVisible = (visible) => {
|
|
156
|
+
const bubbles = splitContactBubbles(visible);
|
|
157
|
+
if (bubbles.length === 0) return;
|
|
158
|
+
while (completedCount < bubbles.length - 1) {
|
|
159
|
+
const bubble = bubbles[completedCount];
|
|
160
|
+
const rest = bubble.slice(activeEmitted.length);
|
|
161
|
+
push(completedCount, rest, true);
|
|
162
|
+
completedCount += 1;
|
|
163
|
+
activeEmitted = '';
|
|
164
|
+
}
|
|
165
|
+
const active = bubbles[bubbles.length - 1] || '';
|
|
166
|
+
if (active.length > activeEmitted.length) {
|
|
167
|
+
push(completedCount, active.slice(activeEmitted.length), false);
|
|
168
|
+
}
|
|
169
|
+
activeEmitted = active;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
get emittedAny() {
|
|
174
|
+
return emittedAny;
|
|
175
|
+
},
|
|
176
|
+
get stopped() {
|
|
177
|
+
return stopped;
|
|
178
|
+
},
|
|
179
|
+
pushDelta(delta) {
|
|
180
|
+
if (stopped || typeof delta !== 'string' || !delta) return;
|
|
181
|
+
const next = accumulated + delta;
|
|
182
|
+
const fenceAt = next.search(CONTACT_FENCE_START);
|
|
183
|
+
if (fenceAt >= 0) {
|
|
184
|
+
const visibleChunk = next.slice(accumulated.length, fenceAt);
|
|
185
|
+
accumulated = next.slice(0, fenceAt);
|
|
186
|
+
if (visibleChunk) applyVisible(accumulated);
|
|
187
|
+
stopped = true;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
accumulated = next;
|
|
191
|
+
applyVisible(accumulated);
|
|
192
|
+
},
|
|
193
|
+
/** Finalize after full stripped chat text is known. */
|
|
194
|
+
finish(finalBubbles) {
|
|
195
|
+
const bubbles = Array.isArray(finalBubbles)
|
|
196
|
+
? finalBubbles.filter((item) => typeof item === 'string' && item.trim())
|
|
197
|
+
: [];
|
|
198
|
+
if (!emittedAny) {
|
|
199
|
+
bubbles.forEach((bubble, index) => push(index, bubble, true));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Complete the active partial bubble, then any trailing bubbles not yet opened.
|
|
203
|
+
if (completedCount < bubbles.length) {
|
|
204
|
+
const current = bubbles[completedCount] || '';
|
|
205
|
+
const rest = current.slice(activeEmitted.length);
|
|
206
|
+
if (rest || activeEmitted || current) push(completedCount, rest, true);
|
|
207
|
+
completedCount += 1;
|
|
208
|
+
activeEmitted = '';
|
|
209
|
+
}
|
|
210
|
+
while (completedCount < bubbles.length) {
|
|
211
|
+
push(completedCount, bubbles[completedCount], true);
|
|
212
|
+
completedCount += 1;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function createContactStreamFn(createChatCompletion, {
|
|
219
|
+
pendingFileParts = [],
|
|
220
|
+
onTextDelta = null,
|
|
221
|
+
onBubbleDelta = null,
|
|
222
|
+
globalEventHub = null,
|
|
223
|
+
bubbleGapMs = 0,
|
|
224
|
+
} = {}) {
|
|
225
|
+
return (model, context) => {
|
|
226
|
+
const stream = createAssistantMessageEventStream();
|
|
227
|
+
const run = async () => {
|
|
228
|
+
try {
|
|
229
|
+
const fallbackFiles = completionFileParts(pendingFileParts);
|
|
230
|
+
const mapped = context.messages.flatMap((message) => {
|
|
231
|
+
if (message.role === 'user') {
|
|
232
|
+
const content = typeof message.content === 'string'
|
|
233
|
+
? message.content
|
|
234
|
+
: (message.content || []).map((part) => part?.text || '').join('');
|
|
235
|
+
const parts = completionFileParts(message.parts);
|
|
236
|
+
if (!content && parts.length === 0) return [];
|
|
237
|
+
return [{
|
|
238
|
+
role: 'user',
|
|
239
|
+
content: content || '[attachment]',
|
|
240
|
+
...(parts.length > 0 ? { parts } : {}),
|
|
241
|
+
}];
|
|
242
|
+
}
|
|
243
|
+
if (message.role === 'assistant') {
|
|
244
|
+
const content = (message.content || [])
|
|
245
|
+
.filter((part) => part?.type === 'text')
|
|
246
|
+
.map((part) => part.text)
|
|
247
|
+
.join('');
|
|
248
|
+
return content ? [{ role: 'assistant', content }] : [];
|
|
249
|
+
}
|
|
250
|
+
if (message.role === 'toolResult') {
|
|
251
|
+
const content = (message.content || [])
|
|
252
|
+
.filter((part) => part?.type === 'text')
|
|
253
|
+
.map((part) => part.text)
|
|
254
|
+
.join('');
|
|
255
|
+
return content ? [{ role: 'user', content: `OpenChamber tool ${message.toolName || 'result'}: ${content}` }] : [];
|
|
256
|
+
}
|
|
257
|
+
return [];
|
|
258
|
+
});
|
|
259
|
+
const messages = [
|
|
260
|
+
...(context.systemPrompt ? [{ role: 'system', content: context.systemPrompt }] : []),
|
|
261
|
+
...mapped,
|
|
262
|
+
];
|
|
263
|
+
const lastUser = [...messages].reverse().find((message) => message.role === 'user');
|
|
264
|
+
if (lastUser && fallbackFiles.length > 0 && !Array.isArray(lastUser.parts)) {
|
|
265
|
+
lastUser.parts = fallbackFiles;
|
|
266
|
+
}
|
|
267
|
+
const bubbleTracker = createContactBubbleDeltaTracker(onBubbleDelta);
|
|
268
|
+
const result = await createChatCompletion({
|
|
269
|
+
body: {
|
|
270
|
+
model: `${model.provider === 'openchamber' ? '' : `${model.provider}/`}${model.id}`.replace(/^\//, '') || model.name,
|
|
271
|
+
providerID: typeof model.name === 'string' && model.name.includes('/')
|
|
272
|
+
? model.name.split('/')[0]
|
|
273
|
+
: undefined,
|
|
274
|
+
modelID: model.id,
|
|
275
|
+
messages,
|
|
276
|
+
},
|
|
277
|
+
onTextDelta: (delta) => {
|
|
278
|
+
if (typeof delta !== 'string' || !delta) return;
|
|
279
|
+
// Do not live-paint raw tokens: they are often chain-of-thought
|
|
280
|
+
// and would appear then vanish after parse/persist.
|
|
281
|
+
if (typeof onTextDelta === 'function') onTextDelta(delta);
|
|
282
|
+
},
|
|
283
|
+
globalEventHub,
|
|
284
|
+
});
|
|
285
|
+
const text = result?.completion?.choices?.[0]?.message?.content
|
|
286
|
+
?? result?.text
|
|
287
|
+
?? '';
|
|
288
|
+
const allowedNames = (context.tools || []).map((tool) => tool?.name).filter(Boolean);
|
|
289
|
+
const parsed = parseContactToolCalls(text, allowedNames);
|
|
290
|
+
if (parsed.toolCall) {
|
|
291
|
+
const toolCall = {
|
|
292
|
+
type: 'toolCall',
|
|
293
|
+
id: `call_${Date.now().toString(36)}`,
|
|
294
|
+
name: parsed.toolCall.name,
|
|
295
|
+
arguments: parsed.toolCall.arguments,
|
|
296
|
+
};
|
|
297
|
+
const spoken = isContactSpokenPreamble(parsed.chatText) ? parsed.chatText.trim() : '';
|
|
298
|
+
const content = spoken
|
|
299
|
+
? [{ type: 'text', text: spoken }, toolCall]
|
|
300
|
+
: [toolCall];
|
|
301
|
+
const partial = {
|
|
302
|
+
...assistantMessage(model, spoken, 'toolUse'),
|
|
303
|
+
content,
|
|
304
|
+
};
|
|
305
|
+
if (spoken) bubbleTracker.finish([spoken]);
|
|
306
|
+
stream.push({ type: 'start', partial });
|
|
307
|
+
if (spoken) {
|
|
308
|
+
stream.push({ type: 'text_start', contentIndex: 0, partial });
|
|
309
|
+
stream.push({ type: 'text_delta', contentIndex: 0, delta: spoken, partial });
|
|
310
|
+
stream.push({ type: 'text_end', contentIndex: 0, content: spoken, partial });
|
|
311
|
+
}
|
|
312
|
+
const toolIndex = spoken ? 1 : 0;
|
|
313
|
+
stream.push({ type: 'toolcall_start', contentIndex: toolIndex, partial });
|
|
314
|
+
stream.push({ type: 'toolcall_delta', contentIndex: toolIndex, delta: JSON.stringify(toolCall.arguments), partial });
|
|
315
|
+
stream.push({ type: 'toolcall_end', contentIndex: toolIndex, toolCall, partial });
|
|
316
|
+
stream.push({ type: 'done', reason: 'toolUse', message: partial });
|
|
317
|
+
stream.end(partial);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const chatText = stripContactToolFences(text);
|
|
321
|
+
const replyBubbles = splitContactBubbles(chatText);
|
|
322
|
+
if (bubbleGapMs > 0 && replyBubbles.length > 1) {
|
|
323
|
+
for (let index = 0; index < replyBubbles.length; index += 1) {
|
|
324
|
+
onBubbleDelta?.(index, replyBubbles[index], true);
|
|
325
|
+
if (index < replyBubbles.length - 1) {
|
|
326
|
+
await new Promise((resolve) => setTimeout(resolve, bubbleGapMs));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
bubbleTracker.finish(replyBubbles);
|
|
331
|
+
}
|
|
332
|
+
const partial = assistantMessage(model, chatText, 'stop');
|
|
333
|
+
stream.push({ type: 'start', partial });
|
|
334
|
+
stream.push({ type: 'text_start', contentIndex: 0, partial });
|
|
335
|
+
stream.push({ type: 'text_delta', contentIndex: 0, delta: chatText, partial });
|
|
336
|
+
stream.push({ type: 'text_end', contentIndex: 0, content: chatText, partial });
|
|
337
|
+
stream.push({ type: 'done', reason: 'stop', message: partial });
|
|
338
|
+
stream.end(partial);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const failed = assistantMessage(model, '', 'error', error?.message || 'upstream_error');
|
|
341
|
+
stream.push({ type: 'error', reason: 'error', error: failed });
|
|
342
|
+
stream.end(failed);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
void run();
|
|
346
|
+
return stream;
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const userMessageText = (message) => {
|
|
351
|
+
if (typeof message?.content === 'string') return message.content;
|
|
352
|
+
return (message?.content || []).map((part) => (typeof part?.text === 'string' ? part.text : '')).join('');
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const assistantTextParts = (message) => (Array.isArray(message?.content) ? message.content : [])
|
|
356
|
+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
357
|
+
.map((part) => part.text)
|
|
358
|
+
.join('');
|
|
359
|
+
|
|
360
|
+
const extractSpokenPreamble = (messages) => {
|
|
361
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
362
|
+
if (message?.role !== 'assistant') continue;
|
|
363
|
+
const parts = Array.isArray(message.content) ? message.content : [];
|
|
364
|
+
if (!parts.some((part) => part?.type === 'toolCall')) continue;
|
|
365
|
+
const text = assistantTextParts(message).trim();
|
|
366
|
+
if (isContactSpokenPreamble(text)) return text;
|
|
367
|
+
}
|
|
368
|
+
return '';
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const extractAssistantText = (messages) => {
|
|
372
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
373
|
+
const message = messages[index];
|
|
374
|
+
if (message?.role !== 'assistant') continue;
|
|
375
|
+
const parts = Array.isArray(message.content) ? message.content : [];
|
|
376
|
+
if (parts.some((part) => part?.type === 'toolCall')) continue;
|
|
377
|
+
const text = assistantTextParts(message);
|
|
378
|
+
if (text.trim()) return text;
|
|
379
|
+
if (message.errorMessage) {
|
|
380
|
+
const error = new Error(message.errorMessage);
|
|
381
|
+
error.code = 'upstream_error';
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return '';
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const extractToolResultText = (messages) => {
|
|
389
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
390
|
+
const message = messages[index];
|
|
391
|
+
if (message?.role !== 'toolResult') continue;
|
|
392
|
+
const text = (message.content || [])
|
|
393
|
+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
394
|
+
.map((part) => part.text)
|
|
395
|
+
.join('')
|
|
396
|
+
.trim();
|
|
397
|
+
if (text) return text;
|
|
398
|
+
}
|
|
399
|
+
return '';
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const extractContactTurnOutcome = (messages, retried) => {
|
|
403
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
404
|
+
let start = 0;
|
|
405
|
+
if (retried) {
|
|
406
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
407
|
+
if (list[index]?.role === 'user') {
|
|
408
|
+
start = index;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (start === 0) {
|
|
413
|
+
const toolIndex = list.findIndex((message) => message?.role === 'toolResult');
|
|
414
|
+
if (toolIndex >= 0) start = toolIndex;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const slice = list.slice(start);
|
|
418
|
+
const cards = extractContactCardsFromMessages(slice);
|
|
419
|
+
const hasTool = contactTurnHasToolResult(slice);
|
|
420
|
+
const spoken = extractSpokenPreamble(slice);
|
|
421
|
+
const confirm = hasTool
|
|
422
|
+
? (extractToolResultText(slice) || extractAssistantText(slice))
|
|
423
|
+
: extractAssistantText(slice);
|
|
424
|
+
const parts = [];
|
|
425
|
+
if (spoken) parts.push(spoken);
|
|
426
|
+
if (confirm && confirm !== spoken) parts.push(confirm);
|
|
427
|
+
return { text: parts.join('\n\n'), cards, hasTool };
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Thin OpenChamber contact harness: pi-agent-core Agent + OpenChamber API
|
|
432
|
+
* tools only + thinkingLevel off. Transcript in, completions via streamFn,
|
|
433
|
+
* bubbles and session cards out. Never bash/edit/read/write.
|
|
434
|
+
*
|
|
435
|
+
* Assigned-session settle reuses the assistants event hub plus session-goal
|
|
436
|
+
* `emitGoalNotification` — read-only into the contact transcript.
|
|
437
|
+
*/
|
|
438
|
+
export async function runContactTurn({
|
|
439
|
+
assistant,
|
|
440
|
+
history,
|
|
441
|
+
userText,
|
|
442
|
+
userParts = [],
|
|
443
|
+
createChatCompletion,
|
|
444
|
+
tools = [],
|
|
445
|
+
projects = [],
|
|
446
|
+
onTextDelta = null,
|
|
447
|
+
onBubbleDelta = null,
|
|
448
|
+
globalEventHub = null,
|
|
449
|
+
AgentImpl = Agent,
|
|
450
|
+
}) {
|
|
451
|
+
const providerID = assistant.providerID;
|
|
452
|
+
const modelID = assistant.modelID;
|
|
453
|
+
const contactTools = Array.isArray(tools)
|
|
454
|
+
? tools.filter((tool) => tool && typeof tool.name === 'string' && !['bash', 'edit', 'read', 'write'].includes(tool.name))
|
|
455
|
+
: [];
|
|
456
|
+
const systemPrompt = [
|
|
457
|
+
CONTACT_SYSTEM_PROMPT,
|
|
458
|
+
formatRegisteredProjectsPrompt(projects),
|
|
459
|
+
formatContactToolsPrompt(contactTools),
|
|
460
|
+
assistant.defaultPrompt,
|
|
461
|
+
].filter((value) => typeof value === 'string' && value.trim()).join('\n\n');
|
|
462
|
+
const model = createContactModel(providerID, modelID);
|
|
463
|
+
// streamFn receives the model; completions needs providerID/modelID.
|
|
464
|
+
model.name = `${providerID}/${modelID}`;
|
|
465
|
+
|
|
466
|
+
const pendingFileParts = [
|
|
467
|
+
...(Array.isArray(history) ? history.flatMap((message) => completionFileParts(message.parts)) : []),
|
|
468
|
+
...completionFileParts(userParts),
|
|
469
|
+
];
|
|
470
|
+
const prior = Array.isArray(history)
|
|
471
|
+
? history.map((message) => (
|
|
472
|
+
message.role === 'assistant'
|
|
473
|
+
? { role: 'assistant', content: [{ type: 'text', text: message.content }], api: model.api, provider: model.provider, model: model.id, usage: emptyUsage(), stopReason: 'stop', timestamp: Date.now() }
|
|
474
|
+
: {
|
|
475
|
+
role: 'user',
|
|
476
|
+
content: message.content,
|
|
477
|
+
...(completionFileParts(message.parts).length > 0 ? { parts: completionFileParts(message.parts) } : {}),
|
|
478
|
+
timestamp: Date.now(),
|
|
479
|
+
}
|
|
480
|
+
))
|
|
481
|
+
: [];
|
|
482
|
+
|
|
483
|
+
const agent = new AgentImpl({
|
|
484
|
+
initialState: {
|
|
485
|
+
systemPrompt,
|
|
486
|
+
model,
|
|
487
|
+
thinkingLevel: 'off',
|
|
488
|
+
tools: contactTools,
|
|
489
|
+
messages: prior,
|
|
490
|
+
},
|
|
491
|
+
streamFn: createContactStreamFn(createChatCompletion, {
|
|
492
|
+
pendingFileParts,
|
|
493
|
+
onTextDelta,
|
|
494
|
+
onBubbleDelta,
|
|
495
|
+
globalEventHub,
|
|
496
|
+
bubbleGapMs: 280,
|
|
497
|
+
}),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
await agent.prompt(userText);
|
|
501
|
+
if (agent.state.errorMessage) {
|
|
502
|
+
const error = new Error(agent.state.errorMessage);
|
|
503
|
+
error.code = 'upstream_error';
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
const requested = detectRequestedContactTools(userText, contactTools.map((tool) => tool.name));
|
|
507
|
+
let retried = false;
|
|
508
|
+
if (requested.length > 0 && !contactTurnHasToolResult(agent.state.messages)) {
|
|
509
|
+
retried = true;
|
|
510
|
+
await agent.prompt(MISSED_FENCE_RETRY_USER_TEXT);
|
|
511
|
+
if (agent.state.errorMessage) {
|
|
512
|
+
const error = new Error(agent.state.errorMessage);
|
|
513
|
+
error.code = 'upstream_error';
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (requested.length > 0 && !contactTurnHasToolResult(agent.state.messages)) {
|
|
518
|
+
return {
|
|
519
|
+
text: MISSED_TOOL_FAILURE_BUBBLE,
|
|
520
|
+
bubbles: [MISSED_TOOL_FAILURE_BUBBLE],
|
|
521
|
+
cards: [],
|
|
522
|
+
thinkingLevel: agent.state.thinkingLevel,
|
|
523
|
+
tools: [...agent.state.tools],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const outcome = extractContactTurnOutcome(agent.state.messages, retried);
|
|
527
|
+
const text = stripContactToolFences(outcome.text);
|
|
528
|
+
if (contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
529
|
+
const bubbles = confirmBubbleAfterContactReset(splitContactBubbles(text));
|
|
530
|
+
return {
|
|
531
|
+
text: bubbles[0] || NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
532
|
+
bubbles,
|
|
533
|
+
cards: [],
|
|
534
|
+
reset: true,
|
|
535
|
+
thinkingLevel: agent.state.thinkingLevel,
|
|
536
|
+
tools: [...agent.state.tools],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (!text.trim() && outcome.cards.length === 0) {
|
|
540
|
+
const error = new Error('Assistant returned no text');
|
|
541
|
+
error.code = 'upstream_error';
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
text,
|
|
546
|
+
bubbles: splitContactBubbles(text),
|
|
547
|
+
cards: outcome.cards,
|
|
548
|
+
thinkingLevel: agent.state.thinkingLevel,
|
|
549
|
+
tools: [...agent.state.tools],
|
|
550
|
+
};
|
|
551
|
+
}
|