@openchambery/web 1.19.13-beta.3 → 1.19.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AssistantView-CKmIS1cS.js +2 -0
- package/dist/assets/{DiagramView-W0IHFk-1.js → DiagramView-BdI4PT2H.js} +1 -1
- package/dist/assets/MarkdownRenderer-CLQ4Z8Z0.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-Buz5LZsa.js → MarkdownRendererImpl-2ewbRZE2.js} +4 -1
- package/dist/assets/MarkstreamRendererImpl-DXewO7S_.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bfs4CTUh.js → MobileDetailNavigation-BGaAyuBH.js} +1 -1
- package/dist/assets/{MobileShareBridge-DtwVLkBL.js → MobileShareBridge-BAZoyeIP.js} +1 -1
- package/dist/assets/MobileSurface-BVb3xNWt.js +2 -0
- package/dist/assets/{MultiRunWindow-yWIHgr89.js → MultiRunWindow-C8ukIdLA.js} +1 -1
- package/dist/assets/{SettingsView-C3BbLpHT.js → SettingsView-CkQ61pbr.js} +1 -1
- package/dist/assets/{SettingsWindow-b0JWdygM.js → SettingsWindow-CSePZ_V8.js} +1 -1
- package/dist/assets/{TerminalView-C74IvmOt.js → TerminalView-BmSIrZ81.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BORu8CtR.js → ToolOutputDialog-2BYaWLo1.js} +1 -1
- package/dist/assets/{appThemeRegistry-DOwhB93n.js → appThemeRegistry-Dk4gBaOu.js} +1 -1
- package/dist/assets/{gitApi-D1mvv14J.js → gitApi-DtPf-h-O.js} +1 -1
- package/dist/assets/{insertionBoundaries-BBn8YW82.js → insertionBoundaries-BMhzPFft.js} +1 -1
- package/dist/assets/{main-Cd7NwXx5.js → main-BjeOhdcG.js} +4 -4
- package/dist/assets/{main-phpzFgMN.js → main-Ci3JmtpW.js} +2 -2
- package/dist/assets/{miniChat-D6UYe50H.js → miniChat-BYrjYEXJ.js} +2 -2
- package/dist/assets/{mobile-w2Ed9I5O.js → mobile-BVMNnPK2.js} +2 -2
- package/dist/assets/{multirun-CYuxJPBX.js → multirun-CmaTeZON.js} +1 -1
- package/dist/assets/{projectMeta-Cir66uF3.js → projectMeta-cw9nrwpr.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BzYqqr9n.js → renderElectronMiniChatApp-COwia8Cs.js} +2 -2
- package/dist/assets/{renderMobileApp-BvIE86_O.js → renderMobileApp-CXwwpY1t.js} +4 -4
- package/dist/assets/{runtimeConfig-BXf9GQ0Q.js → runtimeConfig-DI8l0VyZ.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-lF5S7U4I.js → runtimeEndpointReset-BkAQXmXJ.js} +1 -1
- package/dist/assets/{sessionLookup-CNRDupYq.js → sessionLookup-Imc995rW.js} +1 -1
- package/dist/assets/{useAppFontEffects-NVyoI_Ei.js → useAppFontEffects-BDFUND4v.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-CP3NmMYG.js → useDesktopWindowControlsLayout-C5hZD1co.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-AUoqSyOJ.js → useEffectiveDirectory-e9HjIqGz.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DANrWekj.js → useMobileNavigationStore-CYbDxRTM.js} +1 -1
- package/dist/assets/{useSessionAutoCleanup-ClvQQPqQ.js → useSessionAutoCleanup-BGy9_AhN.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DOk4cAxv.js → useWorkerHighlightedLines-DTGE0CDs.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +20 -11
- package/server/lib/assistants/assign.js +56 -1
- package/server/lib/assistants/assign.test.js +60 -0
- package/server/lib/assistants/contact-tools.js +212 -1
- package/server/lib/assistants/contact-tools.test.js +151 -0
- package/server/lib/assistants/harness.js +56 -5
- package/server/lib/assistants/harness.test.js +17 -9
- package/server/lib/assistants/service.js +514 -16
- package/server/lib/assistants/service.test.js +546 -26
- package/dist/assets/AssistantView-DOZPK9oh.js +0 -2
- package/dist/assets/MarkdownRenderer-v7n_D6nL.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-D4nUXYpP.js +0 -3
- package/dist/assets/MobileSurface-BmYYdY3g.js +0 -2
|
@@ -30,6 +30,8 @@ export const NEW_CONVERSATION_TOOL_NAME = 'new_conversation';
|
|
|
30
30
|
export const CLEAR_CHAT_HISTORY_TOOL_NAME = 'clear_chat_history';
|
|
31
31
|
export const LIST_PROJECTS_TOOL_NAME = 'list_projects';
|
|
32
32
|
export const LIST_SESSIONS_TOOL_NAME = 'list_sessions';
|
|
33
|
+
export const GET_ASSISTANT_SETTINGS_TOOL_NAME = 'get_assistant_settings';
|
|
34
|
+
export const UPDATE_DEFAULT_PROMPT_TOOL_NAME = 'update_default_prompt';
|
|
33
35
|
const CONTACT_TOOL_FENCE = 'openchamber-tool';
|
|
34
36
|
export const ASSIGNED_SESSION_FALLBACK_BUBBLE = 'Opened a coding session.';
|
|
35
37
|
/** Same-turn duplicate assign (different args after a success, or parallel mismatch). */
|
|
@@ -38,6 +40,9 @@ export const ASSIGN_DUPLICATE_TURN_MESSAGE = 'This contact turn already opened a
|
|
|
38
40
|
export const NEW_CONVERSATION_CONFIRM_BUBBLE = 'Memory cleared. Previous messages stay in the chat; I will not use them as context.';
|
|
39
41
|
/** Explicit transcript wipe confirm (clear_chat_history / POST contact/reset). */
|
|
40
42
|
export const CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE = 'Chat history cleared.';
|
|
43
|
+
/** Default-prompt write confirm: persisted settings, next turns only. */
|
|
44
|
+
export const UPDATE_DEFAULT_PROMPT_CONFIRM_BUBBLE = 'Default prompt saved. It applies on later turns (this turn already built its system prompt).';
|
|
45
|
+
export const UPDATE_DEFAULT_PROMPT_UNCHANGED_BUBBLE = 'Default prompt is already that value — nothing changed.';
|
|
41
46
|
const LIST_SESSIONS_LIMIT_DEFAULT = 20;
|
|
42
47
|
const LIST_SESSIONS_LIMIT_MAX = 50;
|
|
43
48
|
|
|
@@ -82,9 +87,23 @@ const CLEAR_CHAT_HISTORY_INTENT = /(?:清空|清除|删除)(?:聊天|对话)记
|
|
|
82
87
|
const NEW_CONVERSATION_INTENT = /开新对话|新对话|清除记忆|清空(?:聊天|对话)(?!记录)|clear memory|clear chat(?! history)|new conversation|start over|forget (?:everything|this|what we|what i)/giu;
|
|
83
88
|
const LIST_PROJECTS_INTENT = /找项目|查项目|看看项目|有哪些项目|项目列表|list projects|find project|registered project|which project/iu;
|
|
84
89
|
const LIST_SESSIONS_INTENT = /现有对话|现有会话|查会话|找会话|会话列表|有哪些会话|list sessions|find (?:a )?session|existing (?:conversation|session|chat)|active (?:conversation|session)/iu;
|
|
90
|
+
// Self settings: missed-fence retry hints only (not authorization).
|
|
91
|
+
const GET_ASSISTANT_SETTINGS_INTENT = /(?:查看|看看|读|显示)(?:一下)?(?:[^。\n]{0,40}?)(?:助手设定|默认提示词|系统提示词|人设)|(?:助手设定|默认提示词|系统提示词|人设)(?:是什么|怎么样)|(?:get|show|read|view) (?:[\w\u4e00-\u9fff]{0,40} )?(?:assistant settings|default prompt|system prompt)/iu;
|
|
92
|
+
const UPDATE_DEFAULT_PROMPT_INTENT = /(?:改|修改|设置|更新|清空)(?:一下)?(?:[^。\n]{0,40}?)(?:默认提示词|系统提示词|人设)|(?:默认提示词|系统提示词|人设)(?:改成|设为|设置为|更新为|清空)|把(?:[^。\n]{0,40}?)(?:默认提示词|系统提示词|人设)(?:改|设|更新|清空)|(?:update|change|set|clear) (?:[\w\u4e00-\u9fff]{0,40} )?(?:default prompt|system prompt)/iu;
|
|
85
93
|
const INTENT_NEGATION = /不要|别|不用|不开|don't|do\s+not/iu;
|
|
86
94
|
const newConversationParameters = typeboxObject({});
|
|
87
95
|
const clearChatHistoryParameters = typeboxObject({});
|
|
96
|
+
const getAssistantSettingsParameters = typeboxObject({
|
|
97
|
+
to: typeboxOptional(typeboxString('Target assistant display name. Omit to read this contact. Same as message_assistant `to`.')),
|
|
98
|
+
name: typeboxOptional(typeboxString('Target assistant display name if `to` is omitted.')),
|
|
99
|
+
toAssistantID: typeboxOptional(typeboxString('Target assistant id when the display name is ambiguous.')),
|
|
100
|
+
});
|
|
101
|
+
const updateDefaultPromptParameters = typeboxObject({
|
|
102
|
+
prompt: typeboxString('New default prompt / system persona. Empty string clears it. Persists to Assistant settings; applies on later turns of that contact.'),
|
|
103
|
+
to: typeboxOptional(typeboxString('Target assistant display name. Omit to update this contact. Example: OpenCode 配置助手.')),
|
|
104
|
+
name: typeboxOptional(typeboxString('Target assistant display name if `to` is omitted.')),
|
|
105
|
+
toAssistantID: typeboxOptional(typeboxString('Target assistant id when the display name is ambiguous.')),
|
|
106
|
+
});
|
|
88
107
|
|
|
89
108
|
const assignParameters = typeboxObject({
|
|
90
109
|
prompt: typeboxString('Coding prompt to kick into the worker OpenCode session. Current-turn user attachments (images/files) are forwarded by the server automatically — do not base64-encode or invent local paths.'),
|
|
@@ -402,6 +421,12 @@ export function detectRequestedContactTools(userText, allowedNames = []) {
|
|
|
402
421
|
if (allowed.has(LIST_SESSIONS_TOOL_NAME) && LIST_SESSIONS_INTENT.test(text)) {
|
|
403
422
|
requested.push(LIST_SESSIONS_TOOL_NAME);
|
|
404
423
|
}
|
|
424
|
+
// Prefer write intent when both settings phrases match (e.g. 把默认提示词改成 X).
|
|
425
|
+
if (allowed.has(UPDATE_DEFAULT_PROMPT_TOOL_NAME) && UPDATE_DEFAULT_PROMPT_INTENT.test(text)) {
|
|
426
|
+
requested.push(UPDATE_DEFAULT_PROMPT_TOOL_NAME);
|
|
427
|
+
} else if (allowed.has(GET_ASSISTANT_SETTINGS_TOOL_NAME) && GET_ASSISTANT_SETTINGS_INTENT.test(text)) {
|
|
428
|
+
requested.push(GET_ASSISTANT_SETTINGS_TOOL_NAME);
|
|
429
|
+
}
|
|
405
430
|
if (allowed.has(CREATE_ASSISTANT_TOOL_NAME) && CREATE_ASSISTANT_INTENT.test(text)) {
|
|
406
431
|
requested.push(CREATE_ASSISTANT_TOOL_NAME);
|
|
407
432
|
}
|
|
@@ -485,6 +510,36 @@ export function resolvePeerAssistant(params = {}, assistants = [], currentAssist
|
|
|
485
510
|
return matches[0];
|
|
486
511
|
}
|
|
487
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Resolve a live assistant for settings read/write.
|
|
515
|
+
* No to/name/toAssistantID → null (caller uses this contact).
|
|
516
|
+
* Name/id may target self or another live assistant.
|
|
517
|
+
*/
|
|
518
|
+
export function resolveAssistantTarget(params = {}, assistants = []) {
|
|
519
|
+
const toAssistantID = trim(params.toAssistantID, 256);
|
|
520
|
+
const toName = trim(params.to, 256) || trim(params.name, 256);
|
|
521
|
+
const list = Array.isArray(assistants) ? assistants : [];
|
|
522
|
+
if (!toAssistantID && !toName) return null;
|
|
523
|
+
if (toAssistantID) {
|
|
524
|
+
const match = list.find((item) => assistantIdentity(item) === toAssistantID);
|
|
525
|
+
if (!match) {
|
|
526
|
+
throw new AssignError('not_found', 'No assistant with that id is available.');
|
|
527
|
+
}
|
|
528
|
+
return match;
|
|
529
|
+
}
|
|
530
|
+
const matches = list.filter((item) => {
|
|
531
|
+
const name = typeof item?.name === 'string' ? item.name.trim() : '';
|
|
532
|
+
return name === toName || name.toLowerCase() === toName.toLowerCase();
|
|
533
|
+
});
|
|
534
|
+
if (matches.length === 0) {
|
|
535
|
+
throw new AssignError('not_found', `No assistant named ${toName} is available.`);
|
|
536
|
+
}
|
|
537
|
+
if (matches.length > 1) {
|
|
538
|
+
throw new AssignError('validation_error', `Several assistants are named ${toName}; use toAssistantID.`);
|
|
539
|
+
}
|
|
540
|
+
return matches[0];
|
|
541
|
+
}
|
|
542
|
+
|
|
488
543
|
/**
|
|
489
544
|
* Serialize a tool parameter schema for the contact system prompt.
|
|
490
545
|
* Accepts plain JSON-schema-like objects and TypeBox schemas (JSON.stringify).
|
|
@@ -525,6 +580,8 @@ export function formatContactToolsPrompt(tools) {
|
|
|
525
580
|
'When they explicitly want to delete the stored chat (清空聊天记录 / clear chat history / delete chat history), call clear_chat_history. Do not use clear_chat_history for ordinary 开新对话 / new conversation wording.',
|
|
526
581
|
'When they want to find a registered project (找项目 / list projects / "openchamber yee"), call list_projects or use the Registered projects block already in context.',
|
|
527
582
|
'When they want existing conversations in a project (现有对话 / list sessions), call list_sessions.',
|
|
583
|
+
'When they want to view assistant settings / default prompt / system persona (查看助手设定 / 默认提示词 / 系统提示词 / 人设), call get_assistant_settings. Omit `to` for this contact; pass to="OpenCode 配置助手" (or toAssistantID) to read another live assistant.',
|
|
584
|
+
'When they want to change a default prompt / system persona (改默认提示词 / 设置人设 / 改某助手的默认提示词), call update_default_prompt. That writes Assistant settings and persists — it is not a one-shot message and not new_conversation. Omit `to` for this contact; pass to/name/toAssistantID to update another live assistant without changing this one.',
|
|
528
585
|
'When they want another assistant (建助理 / create an assistant), call create_assistant.',
|
|
529
586
|
'When they want a separate Chat coding session (建会话 / open a session / 开个新会话), call assign_session after matching the project. File and shell work in this assistant\'s working directory uses read, write, edit, and bash — not assign_session.',
|
|
530
587
|
'When they want a scheduled task (排定时任务 / schedule daily ping), call schedule_task.',
|
|
@@ -543,6 +600,12 @@ export function formatContactToolsPrompt(tools) {
|
|
|
543
600
|
`{"name":"${LIST_SESSIONS_TOOL_NAME}","arguments":{"projectPath":"/path/to/repo","query":"login"}}`,
|
|
544
601
|
'```',
|
|
545
602
|
`\`\`\`${CONTACT_TOOL_FENCE}`,
|
|
603
|
+
`{"name":"${GET_ASSISTANT_SETTINGS_TOOL_NAME}","arguments":{}}`,
|
|
604
|
+
'```',
|
|
605
|
+
`\`\`\`${CONTACT_TOOL_FENCE}`,
|
|
606
|
+
`{"name":"${UPDATE_DEFAULT_PROMPT_TOOL_NAME}","arguments":{"to":"OpenCode 配置助手","prompt":"Be terse and reply in Chinese."}}`,
|
|
607
|
+
'```',
|
|
608
|
+
`\`\`\`${CONTACT_TOOL_FENCE}`,
|
|
546
609
|
`{"name":"${CREATE_ASSISTANT_TOOL_NAME}","arguments":{"name":"FlowQA","model":"opencode-go/deepseek-v4-flash"}}`,
|
|
547
610
|
'```',
|
|
548
611
|
`\`\`\`${CONTACT_TOOL_FENCE}`,
|
|
@@ -554,12 +617,14 @@ export function formatContactToolsPrompt(tools) {
|
|
|
554
617
|
`\`\`\`${CONTACT_TOOL_FENCE}`,
|
|
555
618
|
`{"name":"${ASSIGN_SESSION_TOOL_NAME}","arguments":{"prompt":"...","projectPath":"...","model":"provider/model-id"}}`,
|
|
556
619
|
'```',
|
|
557
|
-
'If the user asked for more than one of these, do them in that order across turns: new_conversation or clear_chat_history, then list_projects, then list_sessions, then create_assistant, then schedule_task, then message_assistant, then assign_session.',
|
|
620
|
+
'If the user asked for more than one of these, do them in that order across turns: new_conversation or clear_chat_history, then list_projects, then list_sessions, then get_assistant_settings or update_default_prompt, then create_assistant, then schedule_task, then message_assistant, then assign_session.',
|
|
558
621
|
'Prerequisite lookups (list_projects / list_sessions) may run before assign_session in the same turn. After a successful assign_session the turn ends — never call assign_session again, and do not keep looping tools.',
|
|
559
622
|
'new_conversation advances this contact\'s LLM context boundary only. Stored messages and watches remain. It never calls session/new or createNew.',
|
|
560
623
|
'clear_chat_history deletes this contact\'s stored messages, parts, and watches. Use only for explicit wipe intent.',
|
|
561
624
|
'You already receive the registered project catalog each turn. Prefer matching label/path yourself; list_projects refreshes or filters. Never claim you cannot see projects; never ask for a raw path when a name matches.',
|
|
562
625
|
'list_sessions searches the OpenChamber session index for existing chats in a project. A failure is not an empty list — surface the error.',
|
|
626
|
+
'get_assistant_settings reads live Assistant settings from storage (id, name, defaultPrompt, provider/model, agent, variant, mode, workspacePath, enabled). Omit to/name/toAssistantID for this contact; pass them to read another live assistant. Use it before claiming what a default prompt is.',
|
|
627
|
+
'update_default_prompt persists defaultPrompt on the target assistant row (empty string clears). Omit to/name/toAssistantID for this contact; pass them to update another live assistant — that does not overwrite this contact. It is Assistant settings — not a one-shot user message and not new_conversation. Takes effect on later turns of that contact only. After success, confirm in one short bubble.',
|
|
563
628
|
'assign_session opens a real OpenChamber/OpenCode session on a registered project (or reuses sessionID). You are not the worker. Optional providerID/modelID/model select the worker only from the connected catalog and never change this contact. Current-turn user attachments are server-forwarded — do not embed base64 or local paths. One successful assign ends this turn.',
|
|
564
629
|
'create_assistant reuses already-connected OpenCode providers (providerID/modelID). Mode is continuous.',
|
|
565
630
|
'schedule_task writes the same payload as PUT /api/projects/:id/scheduled-tasks onto a registered project.',
|
|
@@ -646,6 +711,63 @@ const toolFailure = (error, fallbackCode, fallbackMessage) => {
|
|
|
646
711
|
};
|
|
647
712
|
};
|
|
648
713
|
|
|
714
|
+
const settingsTargetParams = (params) => Boolean(
|
|
715
|
+
trim(params?.toAssistantID, 256) || trim(params?.to, 256) || trim(params?.name, 256),
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const resolveSettingsAssistantTarget = async (params, listAssistants) => {
|
|
719
|
+
const toAssistantID = trim(params?.toAssistantID, 256);
|
|
720
|
+
const toName = trim(params?.to, 256) || trim(params?.name, 256);
|
|
721
|
+
if (!toAssistantID && !toName) return null;
|
|
722
|
+
if (typeof listAssistants !== 'function') {
|
|
723
|
+
throw new AssignError('upstream_error', 'Listing assistants is unavailable.');
|
|
724
|
+
}
|
|
725
|
+
const listed = await listAssistants();
|
|
726
|
+
return resolveAssistantTarget(params, listed);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
/** Subset of AssistantDTO fields exposed to get/update settings tools. */
|
|
730
|
+
export function pickAssistantSettings(dto) {
|
|
731
|
+
if (!dto || typeof dto !== 'object') return null;
|
|
732
|
+
const id = typeof dto.id === 'string' && dto.id.trim()
|
|
733
|
+
? dto.id.trim()
|
|
734
|
+
: (typeof dto.assistantID === 'string' && dto.assistantID.trim() ? dto.assistantID.trim() : null);
|
|
735
|
+
if (!id) return null;
|
|
736
|
+
return {
|
|
737
|
+
id,
|
|
738
|
+
name: typeof dto.name === 'string' ? dto.name : '',
|
|
739
|
+
defaultPrompt: typeof dto.defaultPrompt === 'string' ? dto.defaultPrompt : '',
|
|
740
|
+
providerID: typeof dto.providerID === 'string' ? dto.providerID : '',
|
|
741
|
+
modelID: typeof dto.modelID === 'string' ? dto.modelID : '',
|
|
742
|
+
agent: dto.agent == null ? null : String(dto.agent),
|
|
743
|
+
variant: dto.variant == null ? null : String(dto.variant),
|
|
744
|
+
mode: dto.mode === 'stateless' ? 'stateless' : 'continuous',
|
|
745
|
+
workspacePath: dto.workspacePath == null ? null : String(dto.workspacePath),
|
|
746
|
+
enabled: Boolean(dto.enabled),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Short readable settings dump for the model (empty defaultPrompt is explicit). */
|
|
751
|
+
export function formatAssistantSettingsContent(settings) {
|
|
752
|
+
const row = pickAssistantSettings(settings) || settings;
|
|
753
|
+
if (!row || typeof row !== 'object') return 'Assistant settings unavailable.';
|
|
754
|
+
const prompt = typeof row.defaultPrompt === 'string' ? row.defaultPrompt : '';
|
|
755
|
+
const promptLine = prompt === '' ? 'defaultPrompt: (empty)' : `defaultPrompt:\n${prompt}`;
|
|
756
|
+
return [
|
|
757
|
+
`Assistant settings for ${row.name || row.id || 'this contact'}:`,
|
|
758
|
+
`id: ${row.id || ''}`,
|
|
759
|
+
`name: ${row.name || ''}`,
|
|
760
|
+
promptLine,
|
|
761
|
+
`providerID: ${row.providerID || ''}`,
|
|
762
|
+
`modelID: ${row.modelID || ''}`,
|
|
763
|
+
`agent: ${row.agent == null ? '(none)' : row.agent}`,
|
|
764
|
+
`variant: ${row.variant == null ? '(none)' : row.variant}`,
|
|
765
|
+
`mode: ${row.mode || 'continuous'}`,
|
|
766
|
+
`workspacePath: ${row.workspacePath == null ? '(managed)' : row.workspacePath}`,
|
|
767
|
+
`enabled: ${row.enabled ? 'true' : 'false'}`,
|
|
768
|
+
].join('\n');
|
|
769
|
+
}
|
|
770
|
+
|
|
649
771
|
export function createContactTools({
|
|
650
772
|
assignWork,
|
|
651
773
|
createAssistant,
|
|
@@ -656,6 +778,8 @@ export function createContactTools({
|
|
|
656
778
|
listAssistants,
|
|
657
779
|
listProjects,
|
|
658
780
|
listSessions,
|
|
781
|
+
readAssistantSettings,
|
|
782
|
+
updateAssistantSettings,
|
|
659
783
|
currentAssistant,
|
|
660
784
|
onCard,
|
|
661
785
|
/** Authoritative current-turn user file parts (server-owned). Forwarded on assign by default. */
|
|
@@ -883,6 +1007,93 @@ export function createContactTools({
|
|
|
883
1007
|
}
|
|
884
1008
|
},
|
|
885
1009
|
},
|
|
1010
|
+
{
|
|
1011
|
+
name: GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
1012
|
+
label: 'Get assistant settings',
|
|
1013
|
+
description: [
|
|
1014
|
+
'Read live Assistant settings from storage (not a stale turn snapshot).',
|
|
1015
|
+
'Omit to/name/toAssistantID to read this contact. Pass to="OpenCode 配置助手" or toAssistantID to read another live assistant.',
|
|
1016
|
+
'Returns id, name, defaultPrompt, providerID, modelID, agent, variant, mode, workspacePath, enabled.',
|
|
1017
|
+
'Use when the user asks about 助手设定 / 默认提示词 / 系统提示词 / 人设 — including another assistant\'s.',
|
|
1018
|
+
].join(' '),
|
|
1019
|
+
parameters: getAssistantSettingsParameters,
|
|
1020
|
+
execute: async (_toolCallId, params) => {
|
|
1021
|
+
try {
|
|
1022
|
+
if (typeof readAssistantSettings !== 'function') {
|
|
1023
|
+
throw new AssignError('upstream_error', 'Reading assistant settings is unavailable.');
|
|
1024
|
+
}
|
|
1025
|
+
const target = settingsTargetParams(params)
|
|
1026
|
+
? await resolveSettingsAssistantTarget(params, listAssistants)
|
|
1027
|
+
: null;
|
|
1028
|
+
const raw = target
|
|
1029
|
+
? await readAssistantSettings({ assistantID: assistantIdentity(target) })
|
|
1030
|
+
: await readAssistantSettings();
|
|
1031
|
+
const settings = pickAssistantSettings(raw?.settings ?? raw);
|
|
1032
|
+
if (!settings) {
|
|
1033
|
+
throw new AssignError('upstream_error', 'Assistant settings failed to load.');
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
content: [{ type: 'text', text: formatAssistantSettingsContent(settings) }],
|
|
1037
|
+
details: { settings },
|
|
1038
|
+
terminate: false,
|
|
1039
|
+
};
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
return toolFailure(error, 'get_assistant_settings_failed', 'Could not read assistant settings.');
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
name: UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
1047
|
+
label: 'Update default prompt',
|
|
1048
|
+
description: [
|
|
1049
|
+
'Persist defaultPrompt (system persona) to Assistant settings for this contact or another live assistant.',
|
|
1050
|
+
'Required prompt string; empty string clears it. Not a one-shot message — takes effect on later turns of that contact.',
|
|
1051
|
+
'Omit to/name/toAssistantID to update this contact. Pass to="OpenCode 配置助手" to update that assistant without changing this one.',
|
|
1052
|
+
'Use when the user asks to 改默认提示词 / 设置人设 / 改某助手的默认提示词.',
|
|
1053
|
+
].join(' '),
|
|
1054
|
+
parameters: updateDefaultPromptParameters,
|
|
1055
|
+
execute: async (_toolCallId, params) => {
|
|
1056
|
+
try {
|
|
1057
|
+
if (typeof updateAssistantSettings !== 'function') {
|
|
1058
|
+
throw new AssignError('upstream_error', 'Updating assistant settings is unavailable.');
|
|
1059
|
+
}
|
|
1060
|
+
if (typeof params?.prompt !== 'string') {
|
|
1061
|
+
throw new AssignError('validation_error', 'update_default_prompt requires prompt (string; empty clears).');
|
|
1062
|
+
}
|
|
1063
|
+
const target = settingsTargetParams(params)
|
|
1064
|
+
? await resolveSettingsAssistantTarget(params, listAssistants)
|
|
1065
|
+
: null;
|
|
1066
|
+
const assistantID = target ? assistantIdentity(target) : null;
|
|
1067
|
+
// Allow empty string (clear). Do not trim here — service normalizes like updateAssistant.
|
|
1068
|
+
const result = await updateAssistantSettings({
|
|
1069
|
+
defaultPrompt: params.prompt,
|
|
1070
|
+
...(assistantID ? { assistantID } : {}),
|
|
1071
|
+
});
|
|
1072
|
+
const defaultPrompt = typeof result?.defaultPrompt === 'string'
|
|
1073
|
+
? result.defaultPrompt
|
|
1074
|
+
: params.prompt;
|
|
1075
|
+
const details = {
|
|
1076
|
+
defaultPrompt,
|
|
1077
|
+
...(assistantID ? { assistantID, name: target?.name || null } : {}),
|
|
1078
|
+
};
|
|
1079
|
+
if (result?.unchanged === true || result?.updated === false) {
|
|
1080
|
+
return {
|
|
1081
|
+
content: [{ type: 'text', text: UPDATE_DEFAULT_PROMPT_UNCHANGED_BUBBLE }],
|
|
1082
|
+
details: { ...details, updated: false, unchanged: true },
|
|
1083
|
+
terminate: false,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
content: [{ type: 'text', text: UPDATE_DEFAULT_PROMPT_CONFIRM_BUBBLE }],
|
|
1088
|
+
details: { ...details, updated: true },
|
|
1089
|
+
// Same as create_assistant / schedule_task: keep the turn open for a short confirm.
|
|
1090
|
+
terminate: false,
|
|
1091
|
+
};
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
return toolFailure(error, 'update_default_prompt_failed', 'Could not update default prompt.');
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
886
1097
|
{
|
|
887
1098
|
name: CREATE_ASSISTANT_TOOL_NAME,
|
|
888
1099
|
label: 'Create assistant',
|
|
@@ -6,12 +6,16 @@ import {
|
|
|
6
6
|
CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
7
7
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
8
8
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
9
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
9
10
|
LIST_PROJECTS_TOOL_NAME,
|
|
10
11
|
LIST_SESSIONS_TOOL_NAME,
|
|
11
12
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
12
13
|
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
13
14
|
NEW_CONVERSATION_TOOL_NAME,
|
|
14
15
|
SCHEDULE_TASK_TOOL_NAME,
|
|
16
|
+
UPDATE_DEFAULT_PROMPT_CONFIRM_BUBBLE,
|
|
17
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
18
|
+
UPDATE_DEFAULT_PROMPT_UNCHANGED_BUBBLE,
|
|
15
19
|
confirmBubbleAfterContactReset,
|
|
16
20
|
contactTurnHasSuccessfulReset,
|
|
17
21
|
createContactTools,
|
|
@@ -89,6 +93,8 @@ describe('contact tool protocol', () => {
|
|
|
89
93
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
90
94
|
LIST_PROJECTS_TOOL_NAME,
|
|
91
95
|
LIST_SESSIONS_TOOL_NAME,
|
|
96
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
97
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
92
98
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
93
99
|
SCHEDULE_TASK_TOOL_NAME,
|
|
94
100
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
@@ -118,6 +124,11 @@ describe('contact tool protocol', () => {
|
|
|
118
124
|
expect(detectRequestedContactTools('找项目 openchamber yee', tools)).toEqual([LIST_PROJECTS_TOOL_NAME]);
|
|
119
125
|
expect(detectRequestedContactTools('看看现有对话', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
|
|
120
126
|
expect(detectRequestedContactTools('list sessions in that project', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
|
|
127
|
+
expect(detectRequestedContactTools('看看我的默认提示词', tools)).toEqual([GET_ASSISTANT_SETTINGS_TOOL_NAME]);
|
|
128
|
+
expect(detectRequestedContactTools('看看 OpenCode 配置助手的默认提示词', tools)).toEqual([GET_ASSISTANT_SETTINGS_TOOL_NAME]);
|
|
129
|
+
expect(detectRequestedContactTools('把默认提示词改成简洁中文', tools)).toEqual([UPDATE_DEFAULT_PROMPT_TOOL_NAME]);
|
|
130
|
+
expect(detectRequestedContactTools('改 OpenCode 配置助手的默认提示词', tools)).toEqual([UPDATE_DEFAULT_PROMPT_TOOL_NAME]);
|
|
131
|
+
expect(detectRequestedContactTools('update default prompt to be terse', tools)).toEqual([UPDATE_DEFAULT_PROMPT_TOOL_NAME]);
|
|
121
132
|
expect(detectRequestedContactTools('开新对话', tools)).not.toContain(ASSIGN_SESSION_TOOL_NAME);
|
|
122
133
|
expect(detectRequestedContactTools('开新对话', tools)).not.toContain(CLEAR_CHAT_HISTORY_TOOL_NAME);
|
|
123
134
|
expect(detectRequestedContactTools('不要开编码 session', tools)).toEqual([]);
|
|
@@ -146,6 +157,8 @@ describe('contact tool protocol', () => {
|
|
|
146
157
|
expect(prompt).toContain('clear_chat_history');
|
|
147
158
|
expect(prompt).toContain('list_projects');
|
|
148
159
|
expect(prompt).toContain('list_sessions');
|
|
160
|
+
expect(prompt).toContain('get_assistant_settings');
|
|
161
|
+
expect(prompt).toContain('update_default_prompt');
|
|
149
162
|
expect(prompt).toContain('create_assistant');
|
|
150
163
|
expect(prompt).toContain('schedule_task');
|
|
151
164
|
expect(prompt).toContain('message_assistant');
|
|
@@ -155,11 +168,15 @@ describe('contact tool protocol', () => {
|
|
|
155
168
|
expect(prompt).toContain('清空聊天记录');
|
|
156
169
|
expect(prompt).toContain('找项目');
|
|
157
170
|
expect(prompt).toContain('现有对话');
|
|
171
|
+
expect(prompt).toContain('默认提示词');
|
|
172
|
+
expect(prompt).toContain('助手设定');
|
|
158
173
|
expect(prompt).toContain('建助理');
|
|
159
174
|
expect(prompt).toContain('排定时任务');
|
|
160
175
|
expect(prompt).toContain('说一声');
|
|
161
176
|
expect(prompt).toContain('session/new');
|
|
162
177
|
expect(prompt).toContain('LLM memory only');
|
|
178
|
+
expect(prompt).toContain('Persists');
|
|
179
|
+
expect(prompt).toContain('later turns');
|
|
163
180
|
expect(prompt).not.toContain('/card');
|
|
164
181
|
expect(prompt).not.toContain('/dm');
|
|
165
182
|
expect(prompt).toContain('A reply without the tool call does nothing');
|
|
@@ -273,6 +290,8 @@ describe('createContactTools', () => {
|
|
|
273
290
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
274
291
|
LIST_PROJECTS_TOOL_NAME,
|
|
275
292
|
LIST_SESSIONS_TOOL_NAME,
|
|
293
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
294
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
276
295
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
277
296
|
SCHEDULE_TASK_TOOL_NAME,
|
|
278
297
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
@@ -535,6 +554,138 @@ describe('createContactTools', () => {
|
|
|
535
554
|
expect(failed.content[0].text).toContain('Session index is unavailable');
|
|
536
555
|
expect(failed.details.sessions).toBeUndefined();
|
|
537
556
|
});
|
|
557
|
+
|
|
558
|
+
it('reads assistant settings via live callback and formats empty defaultPrompt', async () => {
|
|
559
|
+
const readAssistantSettings = vi.fn(async () => ({
|
|
560
|
+
id: 'asst_host',
|
|
561
|
+
name: 'DeepSeekQA',
|
|
562
|
+
defaultPrompt: '',
|
|
563
|
+
providerID: 'p',
|
|
564
|
+
modelID: 'm',
|
|
565
|
+
agent: null,
|
|
566
|
+
variant: null,
|
|
567
|
+
mode: 'continuous',
|
|
568
|
+
workspacePath: null,
|
|
569
|
+
enabled: true,
|
|
570
|
+
}));
|
|
571
|
+
const tools = createContactTools({ readAssistantSettings });
|
|
572
|
+
const result = await tools.find((tool) => tool.name === GET_ASSISTANT_SETTINGS_TOOL_NAME).execute('call_get', {});
|
|
573
|
+
expect(readAssistantSettings).toHaveBeenCalledTimes(1);
|
|
574
|
+
expect(result.details.settings).toMatchObject({
|
|
575
|
+
id: 'asst_host',
|
|
576
|
+
name: 'DeepSeekQA',
|
|
577
|
+
defaultPrompt: '',
|
|
578
|
+
providerID: 'p',
|
|
579
|
+
modelID: 'm',
|
|
580
|
+
enabled: true,
|
|
581
|
+
});
|
|
582
|
+
expect(result.content[0].text).toContain('defaultPrompt: (empty)');
|
|
583
|
+
expect(result.terminate).toBe(false);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('updates default prompt, no-ops when unchanged, and reports callback failures', async () => {
|
|
587
|
+
const updateAssistantSettings = vi.fn(async ({ defaultPrompt }) => {
|
|
588
|
+
if (defaultPrompt === 'same') {
|
|
589
|
+
return { updated: false, unchanged: true, defaultPrompt: 'same' };
|
|
590
|
+
}
|
|
591
|
+
return { updated: true, defaultPrompt };
|
|
592
|
+
});
|
|
593
|
+
const tools = createContactTools({ updateAssistantSettings });
|
|
594
|
+
const update = tools.find((tool) => tool.name === UPDATE_DEFAULT_PROMPT_TOOL_NAME);
|
|
595
|
+
|
|
596
|
+
const saved = await update.execute('call_set', { prompt: 'Be terse.' });
|
|
597
|
+
expect(updateAssistantSettings).toHaveBeenCalledWith({ defaultPrompt: 'Be terse.' });
|
|
598
|
+
expect(saved.details).toEqual({ updated: true, defaultPrompt: 'Be terse.' });
|
|
599
|
+
expect(saved.content[0].text).toBe(UPDATE_DEFAULT_PROMPT_CONFIRM_BUBBLE);
|
|
600
|
+
expect(saved.terminate).toBe(false);
|
|
601
|
+
|
|
602
|
+
const cleared = await update.execute('call_clear', { prompt: '' });
|
|
603
|
+
expect(updateAssistantSettings).toHaveBeenCalledWith({ defaultPrompt: '' });
|
|
604
|
+
expect(cleared.details).toEqual({ updated: true, defaultPrompt: '' });
|
|
605
|
+
expect(cleared.terminate).toBe(false);
|
|
606
|
+
|
|
607
|
+
const unchanged = await update.execute('call_same', { prompt: 'same' });
|
|
608
|
+
expect(unchanged.details).toEqual({ updated: false, unchanged: true, defaultPrompt: 'same' });
|
|
609
|
+
expect(unchanged.content[0].text).toBe(UPDATE_DEFAULT_PROMPT_UNCHANGED_BUBBLE);
|
|
610
|
+
expect(unchanged.terminate).toBe(false);
|
|
611
|
+
|
|
612
|
+
const missing = await update.execute('call_missing', {});
|
|
613
|
+
expect(missing.details.error).toBe('validation_error');
|
|
614
|
+
expect(missing.terminate).toBe(true);
|
|
615
|
+
|
|
616
|
+
updateAssistantSettings.mockRejectedValueOnce(Object.assign(new Error('revision_conflict'), { code: 'revision_conflict' }));
|
|
617
|
+
const conflicted = await update.execute('call_conflict', { prompt: 'X' });
|
|
618
|
+
expect(conflicted.details.error).toBe('revision_conflict');
|
|
619
|
+
expect(conflicted.terminate).toBe(true);
|
|
620
|
+
|
|
621
|
+
updateAssistantSettings.mockRejectedValueOnce(Object.assign(new Error('sqlite busy'), { code: 'upstream_error' }));
|
|
622
|
+
const storageFailed = await update.execute('call_storage', { prompt: 'Y' });
|
|
623
|
+
expect(storageFailed.details.error).toBe('upstream_error');
|
|
624
|
+
expect(storageFailed.terminate).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('reads and updates another assistant defaultPrompt by name without changing self', async () => {
|
|
628
|
+
const peer = {
|
|
629
|
+
id: 'asst_peer',
|
|
630
|
+
name: 'OpenCode 配置助手',
|
|
631
|
+
defaultPrompt: 'old peer',
|
|
632
|
+
providerID: 'p',
|
|
633
|
+
modelID: 'm',
|
|
634
|
+
agent: null,
|
|
635
|
+
variant: null,
|
|
636
|
+
mode: 'continuous',
|
|
637
|
+
workspacePath: null,
|
|
638
|
+
enabled: true,
|
|
639
|
+
};
|
|
640
|
+
const host = {
|
|
641
|
+
id: 'asst_host',
|
|
642
|
+
name: 'Host',
|
|
643
|
+
defaultPrompt: 'host prompt',
|
|
644
|
+
providerID: 'p',
|
|
645
|
+
modelID: 'm',
|
|
646
|
+
agent: null,
|
|
647
|
+
variant: null,
|
|
648
|
+
mode: 'continuous',
|
|
649
|
+
workspacePath: null,
|
|
650
|
+
enabled: true,
|
|
651
|
+
};
|
|
652
|
+
const readAssistantSettings = vi.fn(async (input) => (
|
|
653
|
+
input?.assistantID === 'asst_peer' ? peer : host
|
|
654
|
+
));
|
|
655
|
+
const updateAssistantSettings = vi.fn(async (patch) => ({
|
|
656
|
+
updated: true,
|
|
657
|
+
defaultPrompt: patch.defaultPrompt,
|
|
658
|
+
}));
|
|
659
|
+
const listAssistants = vi.fn(async () => [host, peer]);
|
|
660
|
+
const tools = createContactTools({
|
|
661
|
+
readAssistantSettings,
|
|
662
|
+
updateAssistantSettings,
|
|
663
|
+
listAssistants,
|
|
664
|
+
currentAssistant: host,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const got = await tools.find((tool) => tool.name === GET_ASSISTANT_SETTINGS_TOOL_NAME)
|
|
668
|
+
.execute('call_peer_get', { to: 'OpenCode 配置助手' });
|
|
669
|
+
expect(readAssistantSettings).toHaveBeenCalledWith({ assistantID: 'asst_peer' });
|
|
670
|
+
expect(got.details.settings).toMatchObject({ id: 'asst_peer', name: 'OpenCode 配置助手', defaultPrompt: 'old peer' });
|
|
671
|
+
|
|
672
|
+
const saved = await tools.find((tool) => tool.name === UPDATE_DEFAULT_PROMPT_TOOL_NAME)
|
|
673
|
+
.execute('call_peer_set', { to: 'OpenCode 配置助手', prompt: 'new peer persona' });
|
|
674
|
+
expect(updateAssistantSettings).toHaveBeenCalledWith({
|
|
675
|
+
defaultPrompt: 'new peer persona',
|
|
676
|
+
assistantID: 'asst_peer',
|
|
677
|
+
});
|
|
678
|
+
expect(saved.details).toMatchObject({
|
|
679
|
+
updated: true,
|
|
680
|
+
defaultPrompt: 'new peer persona',
|
|
681
|
+
assistantID: 'asst_peer',
|
|
682
|
+
name: 'OpenCode 配置助手',
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const missing = await tools.find((tool) => tool.name === UPDATE_DEFAULT_PROMPT_TOOL_NAME)
|
|
686
|
+
.execute('call_missing_peer', { to: '没有这个助手', prompt: 'x' });
|
|
687
|
+
expect(missing.details.error).toBe('not_found');
|
|
688
|
+
});
|
|
538
689
|
});
|
|
539
690
|
|
|
540
691
|
describe('confirmBubbleAfterContactReset', () => {
|
|
@@ -1,21 +1,29 @@
|
|
|
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,
|
|
18
25
|
stripContactToolFences,
|
|
26
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
19
27
|
} from './contact-tools.js';
|
|
20
28
|
import {
|
|
21
29
|
createPiCodingRuntime,
|
|
@@ -78,7 +86,7 @@ export const CONTACT_SYSTEM_PROMPT = [
|
|
|
78
86
|
'Never write chain-of-thought, plans, tool names, or English narration of what you will do. The user never sees thinking.',
|
|
79
87
|
'Do not expose tool traces, Activity, or editor actions.',
|
|
80
88
|
'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.',
|
|
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.',
|
|
82
90
|
'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
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.',
|
|
84
92
|
'A reply without the tool call does nothing. Never say 已创建, created, scheduled, or opened unless the tool already returned success.',
|
|
@@ -417,6 +425,22 @@ const extractToolResultText = (messages) => {
|
|
|
417
425
|
return '';
|
|
418
426
|
};
|
|
419
427
|
|
|
428
|
+
/** Tool confirms that are themselves the user-facing bubble (no card). */
|
|
429
|
+
const TOOL_TEXT_BUBBLE_TOOLS = new Set([
|
|
430
|
+
NEW_CONVERSATION_TOOL_NAME,
|
|
431
|
+
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
432
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
433
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
/** Card / side-effect tools: never paint English toolText into the transcript. */
|
|
437
|
+
const CARD_SIDE_EFFECT_TOOLS = new Set([
|
|
438
|
+
ASSIGN_SESSION_TOOL_NAME,
|
|
439
|
+
CREATE_ASSISTANT_TOOL_NAME,
|
|
440
|
+
SCHEDULE_TASK_TOOL_NAME,
|
|
441
|
+
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
442
|
+
]);
|
|
443
|
+
|
|
420
444
|
const extractContactTurnOutcome = (messages, retried) => {
|
|
421
445
|
const list = Array.isArray(messages) ? messages : [];
|
|
422
446
|
let start = 0;
|
|
@@ -440,9 +464,22 @@ const extractContactTurnOutcome = (messages, retried) => {
|
|
|
440
464
|
const coding = isPiCodingToolName(lastToolName);
|
|
441
465
|
const toolText = extractToolResultText(slice);
|
|
442
466
|
const assistant = extractAssistantText(slice);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
467
|
+
let confirm = '';
|
|
468
|
+
if (hasTool && !coding) {
|
|
469
|
+
if (TOOL_TEXT_BUBBLE_TOOLS.has(lastToolName)) {
|
|
470
|
+
// Confirm-only tools: toolText is the user bubble.
|
|
471
|
+
confirm = toolText || assistant;
|
|
472
|
+
} else if (CARD_SIDE_EFFECT_TOOLS.has(lastToolName)) {
|
|
473
|
+
// Spoken preamble only. Card is enough when present; never English toolText.
|
|
474
|
+
// Post-tool assistant text (terminate:false tools) may still surface when no spoken.
|
|
475
|
+
confirm = spoken ? '' : (assistant || '');
|
|
476
|
+
} else {
|
|
477
|
+
// list_projects / list_sessions and other app tools keep toolText.
|
|
478
|
+
confirm = toolText || assistant;
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
confirm = assistant || (coding ? toolText : '');
|
|
482
|
+
}
|
|
446
483
|
const parts = [];
|
|
447
484
|
if (spoken) parts.push(spoken);
|
|
448
485
|
if (confirm && confirm !== spoken) parts.push(confirm);
|
|
@@ -554,6 +591,19 @@ export async function runContactTurn({
|
|
|
554
591
|
}
|
|
555
592
|
}
|
|
556
593
|
if (requested.length > 0 && !hasRequestedResult() && !contactTurnHasSuccessfulReset(agent.state.messages)) {
|
|
594
|
+
// Prefer any spoken/assistant text already in the turn over the English failure fallback.
|
|
595
|
+
const missed = extractContactTurnOutcome(agent.state.messages, retried);
|
|
596
|
+
const missedText = stripContactToolFences(missed.text).trim();
|
|
597
|
+
if (missedText) {
|
|
598
|
+
const missedBubbles = splitContactBubbles(missedText);
|
|
599
|
+
return {
|
|
600
|
+
text: missedText,
|
|
601
|
+
bubbles: missedBubbles.length > 0 ? missedBubbles : [missedText],
|
|
602
|
+
cards: [],
|
|
603
|
+
thinkingLevel: agent.state.thinkingLevel,
|
|
604
|
+
tools: [...agent.state.tools],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
557
607
|
return {
|
|
558
608
|
text: MISSED_TOOL_FAILURE_BUBBLE,
|
|
559
609
|
bubbles: [MISSED_TOOL_FAILURE_BUBBLE],
|
|
@@ -580,7 +630,8 @@ export async function runContactTurn({
|
|
|
580
630
|
tools: [...agent.state.tools],
|
|
581
631
|
};
|
|
582
632
|
}
|
|
583
|
-
|
|
633
|
+
// Card tools / side-effect tools may finish with cards only (no English toolText bubble).
|
|
634
|
+
if (!text.trim() && outcome.cards.length === 0 && !outcome.hasTool) {
|
|
584
635
|
const error = new Error('Assistant returned no text');
|
|
585
636
|
error.code = 'upstream_error';
|
|
586
637
|
throw error;
|