@yeaft/webchat-agent 0.1.478 → 0.1.480
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/package.json +1 -1
- package/unify/engine.js +39 -11
- package/unify/prompts.js +18 -21
- package/unify/session.js +6 -0
- package/unify/threads/store.js +181 -0
- package/unify/tools/index.js +17 -0
- package/unify/tools/registry.js +14 -36
- package/unify/tools/spawn-task-tools.js +125 -0
- package/unify/tools/thread-tools.js +183 -0
- package/unify/web-bridge.js +15 -14
package/package.json
CHANGED
package/unify/engine.js
CHANGED
|
@@ -24,6 +24,7 @@ import { recall } from './memory/recall.js';
|
|
|
24
24
|
import { shouldConsolidate, consolidate } from './memory/consolidate.js';
|
|
25
25
|
import { buildMemoryInjection } from './memory/layout.js';
|
|
26
26
|
import { runStopHooks } from './stop-hooks.js';
|
|
27
|
+
import { getThreadStore, MAIN_THREAD_ID } from './threads/store.js';
|
|
27
28
|
|
|
28
29
|
/** Maximum number of turns before the engine stops to prevent infinite loops. */
|
|
29
30
|
const MAX_TURNS = 25;
|
|
@@ -140,14 +141,16 @@ export class Engine {
|
|
|
140
141
|
|
|
141
142
|
/**
|
|
142
143
|
* Get the list of registered tool definitions (for passing to the adapter).
|
|
143
|
-
* Prefers ToolRegistry
|
|
144
|
+
* Prefers ToolRegistry when available, falls back to legacy #tools Map.
|
|
145
|
+
*
|
|
146
|
+
* task-297: mode-based filtering was removed — all registered tools are
|
|
147
|
+
* always exposed to the LLM.
|
|
144
148
|
*
|
|
145
|
-
* @param {string} [mode]
|
|
146
149
|
* @returns {import('./llm/adapter.js').UnifiedToolDef[]}
|
|
147
150
|
*/
|
|
148
|
-
#getToolDefs(
|
|
151
|
+
#getToolDefs() {
|
|
149
152
|
if (this.#toolRegistry) {
|
|
150
|
-
return this.#toolRegistry.getToolDefs(
|
|
153
|
+
return this.#toolRegistry.getToolDefs();
|
|
151
154
|
}
|
|
152
155
|
// Legacy path: no mode filtering
|
|
153
156
|
const defs = [];
|
|
@@ -180,7 +183,7 @@ export class Engine {
|
|
|
180
183
|
|
|
181
184
|
// Get tool names from the appropriate source
|
|
182
185
|
const toolNames = this.#toolRegistry
|
|
183
|
-
? this.#toolRegistry.getToolNames(
|
|
186
|
+
? this.#toolRegistry.getToolNames()
|
|
184
187
|
: Array.from(this.#tools.keys());
|
|
185
188
|
|
|
186
189
|
return buildSystemPrompt({
|
|
@@ -269,11 +272,22 @@ export class Engine {
|
|
|
269
272
|
if (!this.#conversationStore) return;
|
|
270
273
|
if (this.#config._readOnly) return;
|
|
271
274
|
|
|
275
|
+
// task-299 Phase 1: tag persisted messages with the current thread.
|
|
276
|
+
// getThreadStore() lazily seeds a default 'main' thread if not yet init'd.
|
|
277
|
+
let threadId = MAIN_THREAD_ID;
|
|
278
|
+
try {
|
|
279
|
+
threadId = getThreadStore().currentId || MAIN_THREAD_ID;
|
|
280
|
+
} catch {
|
|
281
|
+
// Defensive: any store failure falls back to 'main' so persistence
|
|
282
|
+
// never breaks because of thread bookkeeping.
|
|
283
|
+
}
|
|
284
|
+
|
|
272
285
|
// Persist user message
|
|
273
286
|
this.#conversationStore.append({
|
|
274
287
|
role: 'user',
|
|
275
288
|
content: userContent,
|
|
276
289
|
mode,
|
|
290
|
+
threadId,
|
|
277
291
|
});
|
|
278
292
|
|
|
279
293
|
// Persist assistant message
|
|
@@ -282,6 +296,7 @@ export class Engine {
|
|
|
282
296
|
content: assistantContent,
|
|
283
297
|
mode,
|
|
284
298
|
model: this.#config.model,
|
|
299
|
+
threadId,
|
|
285
300
|
};
|
|
286
301
|
if (toolCalls && toolCalls.length > 0) {
|
|
287
302
|
assistantMsg.toolCalls = toolCalls;
|
|
@@ -326,7 +341,7 @@ export class Engine {
|
|
|
326
341
|
* @param {{ prompt: string, mode?: string, messages?: Array, signal?: AbortSignal }} params
|
|
327
342
|
* @yields {EngineEvent}
|
|
328
343
|
*/
|
|
329
|
-
async *query({ prompt, mode
|
|
344
|
+
async *query({ prompt, mode, messages = [], signal }) {
|
|
330
345
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
331
346
|
yield {
|
|
332
347
|
type: 'error',
|
|
@@ -367,7 +382,7 @@ export class Engine {
|
|
|
367
382
|
{ role: 'user', content: prompt },
|
|
368
383
|
];
|
|
369
384
|
|
|
370
|
-
const toolDefs = this.#getToolDefs(
|
|
385
|
+
const toolDefs = this.#getToolDefs();
|
|
371
386
|
let turnNumber = 0;
|
|
372
387
|
let continueTurns = 0; // auto-continue counter
|
|
373
388
|
let fullResponseText = '';
|
|
@@ -601,21 +616,21 @@ export class Engine {
|
|
|
601
616
|
if (!hasTool) {
|
|
602
617
|
output = `Error: unknown tool "${tc.name}"`;
|
|
603
618
|
isError = true;
|
|
604
|
-
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true };
|
|
619
|
+
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, threadId: this.currentThreadId };
|
|
605
620
|
} else {
|
|
606
621
|
try {
|
|
607
|
-
yield { type: 'tool_start', id: tc.id, name: tc.name, input: tc.input };
|
|
622
|
+
yield { type: 'tool_start', id: tc.id, name: tc.name, input: tc.input, threadId: this.currentThreadId };
|
|
608
623
|
if (this.#toolRegistry) {
|
|
609
624
|
output = await this.#toolRegistry.execute(tc.name, tc.input, toolCtx);
|
|
610
625
|
} else {
|
|
611
626
|
const tool = this.#tools.get(tc.name);
|
|
612
627
|
output = await tool.execute(tc.input, { signal });
|
|
613
628
|
}
|
|
614
|
-
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: false };
|
|
629
|
+
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: false, threadId: this.currentThreadId };
|
|
615
630
|
} catch (err) {
|
|
616
631
|
output = `Error: ${err.message}`;
|
|
617
632
|
isError = true;
|
|
618
|
-
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true };
|
|
633
|
+
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, threadId: this.currentThreadId };
|
|
619
634
|
}
|
|
620
635
|
}
|
|
621
636
|
|
|
@@ -687,6 +702,19 @@ export class Engine {
|
|
|
687
702
|
/** @returns {import('./mcp.js').MCPManager|null} */
|
|
688
703
|
get mcpManager() { return this.#mcpManager; }
|
|
689
704
|
|
|
705
|
+
/**
|
|
706
|
+
* task-299 Phase 1: the engine's current thread marker.
|
|
707
|
+
* Defaults to 'main' if the thread store is unreachable for any reason.
|
|
708
|
+
* @returns {string}
|
|
709
|
+
*/
|
|
710
|
+
get currentThreadId() {
|
|
711
|
+
try {
|
|
712
|
+
return getThreadStore().currentId || MAIN_THREAD_ID;
|
|
713
|
+
} catch {
|
|
714
|
+
return MAIN_THREAD_ID;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
690
718
|
/** @returns {string|null} */
|
|
691
719
|
get yeaftDir() { return this.#yeaftDir; }
|
|
692
720
|
|
package/unify/prompts.js
CHANGED
|
@@ -95,8 +95,7 @@ function extractLangSection(content, language) {
|
|
|
95
95
|
/** Loaded templates — read once at module load time. */
|
|
96
96
|
const RAW_TEMPLATES = {
|
|
97
97
|
base: readTemplate('base.md'),
|
|
98
|
-
|
|
99
|
-
modeWorker: readTemplate('mode-worker.md'),
|
|
98
|
+
modeUnified: readTemplate('mode-unified.md'),
|
|
100
99
|
modeDream: readTemplate('mode-dream.md'),
|
|
101
100
|
toolGuidance: readTemplate('tool-guidance.md'),
|
|
102
101
|
};
|
|
@@ -118,9 +117,7 @@ function getTemplate(key, language) {
|
|
|
118
117
|
const PROMPTS = {
|
|
119
118
|
en: {
|
|
120
119
|
identity: 'You are Yeaft, a helpful AI assistant.',
|
|
121
|
-
mode: (mode) => `Current mode: ${mode}`,
|
|
122
120
|
date: (d) => `Date: ${d}`,
|
|
123
|
-
work: 'You are in work mode. Break tasks into steps, execute them using tools, and report progress.',
|
|
124
121
|
dream: 'You are in dream mode. Reflect on past conversations and consolidate memories.',
|
|
125
122
|
tools: (names) => `Available tools: ${names}`,
|
|
126
123
|
memoryHeader: '## User Memory',
|
|
@@ -130,9 +127,7 @@ const PROMPTS = {
|
|
|
130
127
|
},
|
|
131
128
|
zh: {
|
|
132
129
|
identity: '你是 Yeaft,一个有用的 AI 助手。',
|
|
133
|
-
mode: (mode) => `当前模式:${mode}`,
|
|
134
130
|
date: (d) => `日期:${d}`,
|
|
135
|
-
work: '你处于工作模式。将任务分解为步骤,使用工具执行,并报告进度。',
|
|
136
131
|
dream: '你处于梦境模式。回顾过去的对话,整理和巩固记忆。',
|
|
137
132
|
tools: (names) => `可用工具:${names}`,
|
|
138
133
|
memoryHeader: '## 用户记忆',
|
|
@@ -146,12 +141,17 @@ const PROMPTS = {
|
|
|
146
141
|
export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
|
|
147
142
|
|
|
148
143
|
/**
|
|
149
|
-
* Build the system prompt for a given language
|
|
144
|
+
* Build the system prompt for a given language.
|
|
145
|
+
*
|
|
146
|
+
* task-297: chat/work mode distinction was removed. The prompt now always uses
|
|
147
|
+
* the unified mode template. The `mode` param is retained for backward compat
|
|
148
|
+
* — only `mode === 'dream'` triggers the dream-mode template (used by background
|
|
149
|
+
* memory maintenance); all other values fall through to unified mode.
|
|
150
150
|
*
|
|
151
151
|
* Prompt structure:
|
|
152
152
|
* 1. Core identity (from template or fallback)
|
|
153
|
-
* 2.
|
|
154
|
-
* 3. Mode-specific behavioral instructions (
|
|
153
|
+
* 2. Date metadata
|
|
154
|
+
* 3. Mode-specific behavioral instructions (unified, or dream)
|
|
155
155
|
* 4. Tool list + tool guidance (from template)
|
|
156
156
|
* 5. Skills section
|
|
157
157
|
* 6. Memory section
|
|
@@ -170,7 +170,7 @@ export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
|
|
|
170
170
|
*/
|
|
171
171
|
export function buildSystemPrompt({
|
|
172
172
|
language = 'en',
|
|
173
|
-
mode
|
|
173
|
+
mode,
|
|
174
174
|
toolNames = [],
|
|
175
175
|
memory,
|
|
176
176
|
memoryInjection,
|
|
@@ -192,23 +192,20 @@ export function buildSystemPrompt({
|
|
|
192
192
|
parts.push(lang.identity);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
// ─── 2.
|
|
196
|
-
parts.push(lang.mode(mode));
|
|
195
|
+
// ─── 2. Date Metadata ──────────────────────────────────
|
|
197
196
|
parts.push(lang.date(new Date().toISOString().split('T')[0]));
|
|
198
197
|
|
|
199
198
|
// ─── 3. Mode-Specific Instructions ─────────────────────
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
} else if (mode === 'dream') {
|
|
199
|
+
// task-297: single unified mode for all normal operation.
|
|
200
|
+
// `dream` is retained for background memory maintenance.
|
|
201
|
+
if (mode === 'dream') {
|
|
204
202
|
const dreamTemplate = getTemplate('modeDream', effectiveLang);
|
|
205
203
|
parts.push(dreamTemplate || lang.dream);
|
|
206
|
-
} else
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
parts.push(
|
|
204
|
+
} else {
|
|
205
|
+
const unifiedTemplate = getTemplate('modeUnified', effectiveLang);
|
|
206
|
+
if (unifiedTemplate) {
|
|
207
|
+
parts.push(unifiedTemplate);
|
|
210
208
|
}
|
|
211
|
-
// No fallback needed — chat mode previously had no instructions
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
// ─── 4. Tools + Tool Guidance ──────────────────────────
|
package/unify/session.js
CHANGED
|
@@ -23,6 +23,7 @@ import { SkillManager, createSkillManager } from './skills.js';
|
|
|
23
23
|
import { MCPManager } from './mcp.js';
|
|
24
24
|
import { createFullRegistry } from './tools/index.js';
|
|
25
25
|
import { initTaskStore } from './tools/task-tools.js';
|
|
26
|
+
import { initThreadStore } from './threads/store.js';
|
|
26
27
|
import { Engine } from './engine.js';
|
|
27
28
|
import { join } from 'path';
|
|
28
29
|
|
|
@@ -121,6 +122,11 @@ export async function loadSession(options = {}) {
|
|
|
121
122
|
// ─── 5a. Initialize task store ─────────────────────────
|
|
122
123
|
initTaskStore(yeaftDir, { readOnly: config._readOnly || false });
|
|
123
124
|
|
|
125
|
+
// ─── 5b. Initialize thread store (task-299 Phase 1) ────
|
|
126
|
+
// In-memory only for Phase 1; replaced by a file-backed store
|
|
127
|
+
// when task-298's data layer merges.
|
|
128
|
+
initThreadStore();
|
|
129
|
+
|
|
124
130
|
// ─── 6. Load skills ────────────────────────────────────
|
|
125
131
|
let skillManager;
|
|
126
132
|
if (skipSkills) {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* store.js — In-memory ThreadStore for Yeaft Unify (Phase 1 mock).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 intentionally keeps the implementation in-memory only: it lets the
|
|
5
|
+
* thread/task spawn tools ship and be tested before task-298's real
|
|
6
|
+
* filesystem layer is merged. When task-298 merges, this module will be
|
|
7
|
+
* replaced (or promoted to a shim) by a file-backed store with the same API.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities (Phase 1):
|
|
10
|
+
* - Maintain a map of threadId → thread metadata.
|
|
11
|
+
* - Track a "currentThreadId" marker for the engine.
|
|
12
|
+
* - Maintain attachments from threadId → taskId.
|
|
13
|
+
*
|
|
14
|
+
* A single "main" thread is created on construction so that pre-spawn
|
|
15
|
+
* messages have a valid threadId to carry.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { randomUUID } from 'crypto';
|
|
19
|
+
|
|
20
|
+
/** Default / root thread id — every fresh ThreadStore has one. */
|
|
21
|
+
export const MAIN_THREAD_ID = 'main';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} Thread
|
|
25
|
+
* @property {string} id
|
|
26
|
+
* @property {string} name
|
|
27
|
+
* @property {string} [goal]
|
|
28
|
+
* @property {string|null} parentThreadId
|
|
29
|
+
* @property {number} createdAt
|
|
30
|
+
* @property {number} updatedAt
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export class ThreadStore {
|
|
34
|
+
/** @type {Map<string, Thread>} */
|
|
35
|
+
#threads;
|
|
36
|
+
|
|
37
|
+
/** @type {string} */
|
|
38
|
+
#currentId;
|
|
39
|
+
|
|
40
|
+
/** @type {Map<string, string>} threadId → taskId */
|
|
41
|
+
#attachments;
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
this.#threads = new Map();
|
|
45
|
+
this.#attachments = new Map();
|
|
46
|
+
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const main = {
|
|
49
|
+
id: MAIN_THREAD_ID,
|
|
50
|
+
name: 'main',
|
|
51
|
+
goal: '',
|
|
52
|
+
parentThreadId: null,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
};
|
|
56
|
+
this.#threads.set(main.id, main);
|
|
57
|
+
this.#currentId = main.id;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Get current thread id (defaults to 'main'). */
|
|
61
|
+
get currentId() {
|
|
62
|
+
return this.#currentId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Total thread count (including 'main'). */
|
|
66
|
+
get size() {
|
|
67
|
+
return this.#threads.size;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a new thread.
|
|
72
|
+
* @param {{ name: string, goal?: string, parentThreadId?: string }} spec
|
|
73
|
+
* @returns {Thread}
|
|
74
|
+
*/
|
|
75
|
+
create({ name, goal = '', parentThreadId = null } = {}) {
|
|
76
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
77
|
+
throw new Error('thread name is required');
|
|
78
|
+
}
|
|
79
|
+
if (parentThreadId && !this.#threads.has(parentThreadId)) {
|
|
80
|
+
throw new Error(`parent thread not found: ${parentThreadId}`);
|
|
81
|
+
}
|
|
82
|
+
const id = `thr-${randomUUID().slice(0, 8)}`;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const thread = {
|
|
85
|
+
id,
|
|
86
|
+
name: name.trim(),
|
|
87
|
+
goal: goal || '',
|
|
88
|
+
parentThreadId: parentThreadId || null,
|
|
89
|
+
createdAt: now,
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
};
|
|
92
|
+
this.#threads.set(id, thread);
|
|
93
|
+
return thread;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {string} id */
|
|
97
|
+
get(id) {
|
|
98
|
+
return this.#threads.get(id) || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @returns {Thread[]} */
|
|
102
|
+
list() {
|
|
103
|
+
return [...this.#threads.values()];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {string} id */
|
|
107
|
+
has(id) {
|
|
108
|
+
return this.#threads.has(id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Set the current thread marker. Throws if unknown.
|
|
113
|
+
* @param {string} id
|
|
114
|
+
*/
|
|
115
|
+
switch(id) {
|
|
116
|
+
if (!this.#threads.has(id)) {
|
|
117
|
+
throw new Error(`thread not found: ${id}`);
|
|
118
|
+
}
|
|
119
|
+
this.#currentId = id;
|
|
120
|
+
const t = this.#threads.get(id);
|
|
121
|
+
t.updatedAt = Date.now();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Attach a task to a thread. Overwrites any existing attachment.
|
|
126
|
+
* @param {string} threadId
|
|
127
|
+
* @param {string} taskId
|
|
128
|
+
*/
|
|
129
|
+
attachTask(threadId, taskId) {
|
|
130
|
+
if (!this.#threads.has(threadId)) {
|
|
131
|
+
throw new Error(`thread not found: ${threadId}`);
|
|
132
|
+
}
|
|
133
|
+
if (!taskId || typeof taskId !== 'string') {
|
|
134
|
+
throw new Error('taskId is required');
|
|
135
|
+
}
|
|
136
|
+
this.#attachments.set(threadId, taskId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the taskId attached to a thread, if any.
|
|
141
|
+
* @param {string} threadId
|
|
142
|
+
* @returns {string|null}
|
|
143
|
+
*/
|
|
144
|
+
attachedTask(threadId) {
|
|
145
|
+
return this.#attachments.get(threadId) || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @returns {Array<{ threadId: string, taskId: string }>} */
|
|
149
|
+
listAttachments() {
|
|
150
|
+
return [...this.#attachments.entries()].map(([threadId, taskId]) => ({ threadId, taskId }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @type {ThreadStore|null} */
|
|
155
|
+
let threadStore = null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Initialize the thread store. Safe to call multiple times — subsequent calls
|
|
159
|
+
* replace the store only if `force` is true (primarily for tests).
|
|
160
|
+
* @param {{ force?: boolean }} [opts]
|
|
161
|
+
* @returns {ThreadStore}
|
|
162
|
+
*/
|
|
163
|
+
export function initThreadStore(opts = {}) {
|
|
164
|
+
if (!threadStore || opts.force) {
|
|
165
|
+
threadStore = new ThreadStore();
|
|
166
|
+
}
|
|
167
|
+
return threadStore;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** @returns {ThreadStore} */
|
|
171
|
+
export function getThreadStore() {
|
|
172
|
+
if (!threadStore) {
|
|
173
|
+
threadStore = new ThreadStore();
|
|
174
|
+
}
|
|
175
|
+
return threadStore;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Test-only reset helper. */
|
|
179
|
+
export function _resetThreadStoreForTests() {
|
|
180
|
+
threadStore = null;
|
|
181
|
+
}
|
package/unify/tools/index.js
CHANGED
|
@@ -55,6 +55,15 @@ import {
|
|
|
55
55
|
updatePlan,
|
|
56
56
|
} from './task-tools.js';
|
|
57
57
|
|
|
58
|
+
// --- P1 Thread tools (task-299 Phase 1) ---
|
|
59
|
+
import {
|
|
60
|
+
spawnThread,
|
|
61
|
+
switchThread,
|
|
62
|
+
listThreads,
|
|
63
|
+
attachThreadToTask,
|
|
64
|
+
} from './thread-tools.js';
|
|
65
|
+
import { spawnTask, spawnSubtask } from './spawn-task-tools.js';
|
|
66
|
+
|
|
58
67
|
// --- P2 Auxiliary tools ---
|
|
59
68
|
import { jsRepl, jsReplReset } from './js-repl.js';
|
|
60
69
|
import notebookEdit from './notebook-edit.js';
|
|
@@ -113,6 +122,14 @@ export const allTools = [
|
|
|
113
122
|
followupTask,
|
|
114
123
|
updatePlan,
|
|
115
124
|
|
|
125
|
+
// P1 Thread (task-299 Phase 1)
|
|
126
|
+
spawnThread,
|
|
127
|
+
switchThread,
|
|
128
|
+
listThreads,
|
|
129
|
+
attachThreadToTask,
|
|
130
|
+
spawnTask,
|
|
131
|
+
spawnSubtask,
|
|
132
|
+
|
|
116
133
|
// P2 Auxiliary
|
|
117
134
|
jsRepl,
|
|
118
135
|
jsReplReset,
|
package/unify/tools/registry.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* registry.js — Tool registration center for Yeaft Unify
|
|
3
3
|
*
|
|
4
|
-
* Manages tool registration
|
|
4
|
+
* Manages tool registration and execution dispatch.
|
|
5
5
|
* The engine uses this to get tool definitions for the LLM and to execute tool calls.
|
|
6
|
+
*
|
|
7
|
+
* NOTE (task-297): Mode-based filtering (chat/work) has been removed. Unify now runs as
|
|
8
|
+
* a single unified mode where all registered tools are available. Tool definitions may
|
|
9
|
+
* still carry a `modes` field, but the registry ignores it.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
/**
|
|
9
|
-
* Mode normalization map.
|
|
10
|
-
* 'coordinator' and 'worker' inherit 'work' tools.
|
|
11
|
-
* 'dream' has no tools by design (returns empty).
|
|
12
|
-
*/
|
|
13
|
-
const MODE_ALIASES = {
|
|
14
|
-
coordinator: 'work',
|
|
15
|
-
worker: 'work',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
12
|
export class ToolRegistry {
|
|
19
13
|
/** @type {Map<string, import('./types.js').ToolDef>} */
|
|
20
14
|
#tools = new Map();
|
|
@@ -69,35 +63,20 @@ export class ToolRegistry {
|
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
/**
|
|
72
|
-
*
|
|
73
|
-
* @param {string} mode
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
|
-
static resolveMode(mode) {
|
|
77
|
-
return MODE_ALIASES[mode] || mode;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get all tools available in a given mode.
|
|
82
|
-
* @param {string} mode
|
|
66
|
+
* Get all registered tools (unfiltered).
|
|
83
67
|
* @returns {import('./types.js').ToolDef[]}
|
|
84
68
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const result = [];
|
|
88
|
-
for (const [, tool] of this.#tools) {
|
|
89
|
-
if (tool.modes.includes(effectiveMode)) result.push(tool);
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
69
|
+
getAllTools() {
|
|
70
|
+
return Array.from(this.#tools.values());
|
|
92
71
|
}
|
|
93
72
|
|
|
94
73
|
/**
|
|
95
74
|
* Get tool definitions for the LLM adapter.
|
|
96
|
-
*
|
|
75
|
+
* Returns all registered tools — mode filtering was removed in task-297.
|
|
97
76
|
* @returns {{ name: string, description: string, parameters: object }[]}
|
|
98
77
|
*/
|
|
99
|
-
getToolDefs(
|
|
100
|
-
return this.
|
|
78
|
+
getToolDefs() {
|
|
79
|
+
return this.getAllTools().map(t => ({
|
|
101
80
|
name: t.name,
|
|
102
81
|
description: t.description,
|
|
103
82
|
parameters: t.parameters,
|
|
@@ -105,12 +84,11 @@ export class ToolRegistry {
|
|
|
105
84
|
}
|
|
106
85
|
|
|
107
86
|
/**
|
|
108
|
-
* Get tool names
|
|
109
|
-
* @param {string} mode
|
|
87
|
+
* Get all registered tool names.
|
|
110
88
|
* @returns {string[]}
|
|
111
89
|
*/
|
|
112
|
-
getToolNames(
|
|
113
|
-
return
|
|
90
|
+
getToolNames() {
|
|
91
|
+
return Array.from(this.#tools.keys());
|
|
114
92
|
}
|
|
115
93
|
|
|
116
94
|
/**
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn-task-tools.js — Thin semantic-sugar wrappers over TaskCreate.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 of task-299: expose `SpawnTask` and `SpawnSubtask` as first-class
|
|
5
|
+
* tool names so the LLM can "spawn" work items in the same vocabulary it
|
|
6
|
+
* uses to spawn threads. Both delegate to the existing TaskStore (shared
|
|
7
|
+
* with TaskCreate), so there is a single source of truth for task data.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineTool } from './types.js';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { getTaskStore } from './task-tools.js';
|
|
13
|
+
|
|
14
|
+
function requireStore() {
|
|
15
|
+
const store = getTaskStore();
|
|
16
|
+
if (!store) return { error: 'Task store not initialized. Session may still be loading.' };
|
|
17
|
+
return { store };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function newTaskId() {
|
|
21
|
+
return `task-${randomUUID().slice(0, 8)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── SpawnTask ──────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const spawnTask = defineTool({
|
|
27
|
+
name: 'SpawnTask',
|
|
28
|
+
description: `Spawn a new top-level task.
|
|
29
|
+
|
|
30
|
+
Equivalent to TaskCreate without a parent. Provided as a first-class
|
|
31
|
+
name so tasks and threads can be "spawned" with a uniform vocabulary.`,
|
|
32
|
+
parameters: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
title: { type: 'string' },
|
|
36
|
+
description: { type: 'string' },
|
|
37
|
+
priority: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: ['low', 'medium', 'high', 'critical'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ['title'],
|
|
43
|
+
},
|
|
44
|
+
modes: ['work'],
|
|
45
|
+
isConcurrencySafe: () => false,
|
|
46
|
+
isReadOnly: () => false,
|
|
47
|
+
async execute(input) {
|
|
48
|
+
const got = requireStore();
|
|
49
|
+
if (got.error) return JSON.stringify({ error: got.error });
|
|
50
|
+
const { title, description = '', priority = 'medium' } = input || {};
|
|
51
|
+
if (!title) return JSON.stringify({ error: 'title is required' });
|
|
52
|
+
|
|
53
|
+
const id = newTaskId();
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
got.store.create({
|
|
56
|
+
id,
|
|
57
|
+
title,
|
|
58
|
+
description,
|
|
59
|
+
priority,
|
|
60
|
+
status: 'pending',
|
|
61
|
+
parentId: null,
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
});
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
success: true,
|
|
67
|
+
task: { id, title, priority, status: 'pending' },
|
|
68
|
+
message: `Task spawned: ${title} (${id})`,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ─── SpawnSubtask ───────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export const spawnSubtask = defineTool({
|
|
76
|
+
name: 'SpawnSubtask',
|
|
77
|
+
description: `Spawn a subtask under an existing parent task.
|
|
78
|
+
|
|
79
|
+
Requires parent_task_id. The parent must exist. Use for breaking a
|
|
80
|
+
larger task into executable pieces.`,
|
|
81
|
+
parameters: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
parent_task_id: { type: 'string' },
|
|
85
|
+
title: { type: 'string' },
|
|
86
|
+
description: { type: 'string' },
|
|
87
|
+
priority: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
enum: ['low', 'medium', 'high', 'critical'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ['parent_task_id', 'title'],
|
|
93
|
+
},
|
|
94
|
+
modes: ['work'],
|
|
95
|
+
isConcurrencySafe: () => false,
|
|
96
|
+
isReadOnly: () => false,
|
|
97
|
+
async execute(input) {
|
|
98
|
+
const got = requireStore();
|
|
99
|
+
if (got.error) return JSON.stringify({ error: got.error });
|
|
100
|
+
const { parent_task_id, title, description = '', priority = 'medium' } = input || {};
|
|
101
|
+
if (!parent_task_id) return JSON.stringify({ error: 'parent_task_id is required' });
|
|
102
|
+
if (!title) return JSON.stringify({ error: 'title is required' });
|
|
103
|
+
|
|
104
|
+
const parent = got.store.get(parent_task_id);
|
|
105
|
+
if (!parent) return JSON.stringify({ error: `Parent task not found: ${parent_task_id}` });
|
|
106
|
+
|
|
107
|
+
const id = newTaskId();
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
got.store.create({
|
|
110
|
+
id,
|
|
111
|
+
title,
|
|
112
|
+
description: description || `Subtask of: ${parent.title}`,
|
|
113
|
+
priority,
|
|
114
|
+
status: 'pending',
|
|
115
|
+
parentId: parent_task_id,
|
|
116
|
+
createdAt: now,
|
|
117
|
+
updatedAt: now,
|
|
118
|
+
});
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
success: true,
|
|
121
|
+
task: { id, title, priority, status: 'pending', parentId: parent_task_id },
|
|
122
|
+
message: `Subtask spawned: ${title} (${id}) under ${parent_task_id}`,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thread-tools.js — Thread-spawning tools for Unify Engine (Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 scope (task-299):
|
|
5
|
+
* - SpawnThread — create a new thread
|
|
6
|
+
* - SwitchThread — set the engine's currentThreadId marker
|
|
7
|
+
* - ListThreads — list threads + current marker
|
|
8
|
+
* - AttachThreadToTask — bind a thread to an existing task
|
|
9
|
+
*
|
|
10
|
+
* Phase 1 uses the in-memory ThreadStore (agent/unify/threads/store.js)
|
|
11
|
+
* so these tools can ship and be tested before task-298's file-backed
|
|
12
|
+
* data layer merges. The tool surface is designed to remain stable when
|
|
13
|
+
* the store is replaced.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { defineTool } from './types.js';
|
|
17
|
+
import { getThreadStore } from '../threads/store.js';
|
|
18
|
+
import { getTaskStore } from './task-tools.js';
|
|
19
|
+
|
|
20
|
+
// ─── SpawnThread ─────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export const spawnThread = defineTool({
|
|
23
|
+
name: 'SpawnThread',
|
|
24
|
+
description: `Create a new thread (conversation track) for parallel work.
|
|
25
|
+
|
|
26
|
+
A thread is a named conversation track that groups related messages and
|
|
27
|
+
tool calls under a single goal. Use when the work needs a fresh focus
|
|
28
|
+
track separate from the current conversation.
|
|
29
|
+
|
|
30
|
+
Returns the new threadId (format: "thr-xxxxxxxx"). Does NOT switch the
|
|
31
|
+
engine to the new thread — call SwitchThread to activate it.`,
|
|
32
|
+
parameters: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
name: { type: 'string', description: 'Short human-readable name' },
|
|
36
|
+
goal: { type: 'string', description: 'Optional one-sentence goal' },
|
|
37
|
+
parent_thread_id: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Optional parent threadId for hierarchy',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ['name'],
|
|
43
|
+
},
|
|
44
|
+
modes: ['work'],
|
|
45
|
+
isConcurrencySafe: () => false,
|
|
46
|
+
isReadOnly: () => false,
|
|
47
|
+
async execute(input) {
|
|
48
|
+
const { name, goal, parent_thread_id } = input || {};
|
|
49
|
+
if (!name) return JSON.stringify({ error: 'name is required' });
|
|
50
|
+
try {
|
|
51
|
+
const store = getThreadStore();
|
|
52
|
+
const t = store.create({ name, goal, parentThreadId: parent_thread_id || null });
|
|
53
|
+
return JSON.stringify({
|
|
54
|
+
success: true,
|
|
55
|
+
thread: {
|
|
56
|
+
id: t.id,
|
|
57
|
+
name: t.name,
|
|
58
|
+
goal: t.goal,
|
|
59
|
+
parentThreadId: t.parentThreadId,
|
|
60
|
+
},
|
|
61
|
+
message: `Thread created: ${t.name} (${t.id})`,
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return JSON.stringify({ error: err.message || String(err) });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ─── SwitchThread ────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export const switchThread = defineTool({
|
|
72
|
+
name: 'SwitchThread',
|
|
73
|
+
description: `Switch the engine's active thread marker.
|
|
74
|
+
|
|
75
|
+
All messages and tool calls persisted after this call will carry the new
|
|
76
|
+
threadId (Phase 1: marker only — the Engine reads it when persisting and
|
|
77
|
+
the web-bridge forwards it to the UI). Use 'main' to return to the root
|
|
78
|
+
thread.`,
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
thread_id: { type: 'string', description: 'Thread id to switch to' },
|
|
83
|
+
},
|
|
84
|
+
required: ['thread_id'],
|
|
85
|
+
},
|
|
86
|
+
modes: ['work'],
|
|
87
|
+
isConcurrencySafe: () => false,
|
|
88
|
+
isReadOnly: () => false,
|
|
89
|
+
async execute(input) {
|
|
90
|
+
const { thread_id } = input || {};
|
|
91
|
+
if (!thread_id) return JSON.stringify({ error: 'thread_id is required' });
|
|
92
|
+
try {
|
|
93
|
+
const store = getThreadStore();
|
|
94
|
+
store.switch(thread_id);
|
|
95
|
+
return JSON.stringify({
|
|
96
|
+
success: true,
|
|
97
|
+
currentThreadId: store.currentId,
|
|
98
|
+
message: `Switched to thread ${thread_id}`,
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return JSON.stringify({ error: err.message || String(err) });
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── ListThreads ─────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export const listThreads = defineTool({
|
|
109
|
+
name: 'ListThreads',
|
|
110
|
+
description: `List all threads and the engine's current thread marker.`,
|
|
111
|
+
parameters: { type: 'object', properties: {} },
|
|
112
|
+
modes: ['work'],
|
|
113
|
+
isConcurrencySafe: () => true,
|
|
114
|
+
isReadOnly: () => true,
|
|
115
|
+
async execute() {
|
|
116
|
+
const store = getThreadStore();
|
|
117
|
+
const threads = store.list().map(t => ({
|
|
118
|
+
id: t.id,
|
|
119
|
+
name: t.name,
|
|
120
|
+
goal: t.goal,
|
|
121
|
+
parentThreadId: t.parentThreadId,
|
|
122
|
+
attachedTaskId: store.attachedTask(t.id),
|
|
123
|
+
}));
|
|
124
|
+
return JSON.stringify(
|
|
125
|
+
{
|
|
126
|
+
currentThreadId: store.currentId,
|
|
127
|
+
threads,
|
|
128
|
+
totalCount: threads.length,
|
|
129
|
+
},
|
|
130
|
+
null,
|
|
131
|
+
2,
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── AttachThreadToTask ──────────────────────────────────
|
|
137
|
+
|
|
138
|
+
export const attachThreadToTask = defineTool({
|
|
139
|
+
name: 'AttachThreadToTask',
|
|
140
|
+
description: `Link an existing thread to an existing task.
|
|
141
|
+
|
|
142
|
+
Use to record which thread is responsible for which task. The thread and
|
|
143
|
+
the task must both already exist. Overwrites any previous attachment for
|
|
144
|
+
the same thread.`,
|
|
145
|
+
parameters: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
thread_id: { type: 'string' },
|
|
149
|
+
task_id: { type: 'string' },
|
|
150
|
+
},
|
|
151
|
+
required: ['thread_id', 'task_id'],
|
|
152
|
+
},
|
|
153
|
+
modes: ['work'],
|
|
154
|
+
isConcurrencySafe: () => false,
|
|
155
|
+
isReadOnly: () => false,
|
|
156
|
+
async execute(input) {
|
|
157
|
+
const { thread_id, task_id } = input || {};
|
|
158
|
+
if (!thread_id) return JSON.stringify({ error: 'thread_id is required' });
|
|
159
|
+
if (!task_id) return JSON.stringify({ error: 'task_id is required' });
|
|
160
|
+
|
|
161
|
+
const taskStore = getTaskStore();
|
|
162
|
+
// If the task store is initialized, validate the task exists; if not
|
|
163
|
+
// initialized (e.g. Phase 1 unit tests running before session bootstrap),
|
|
164
|
+
// skip the task existence check — we still enforce thread existence.
|
|
165
|
+
if (taskStore) {
|
|
166
|
+
const task = taskStore.get(task_id);
|
|
167
|
+
if (!task) return JSON.stringify({ error: `Task not found: ${task_id}` });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const store = getThreadStore();
|
|
172
|
+
store.attachTask(thread_id, task_id);
|
|
173
|
+
return JSON.stringify({
|
|
174
|
+
success: true,
|
|
175
|
+
threadId: thread_id,
|
|
176
|
+
taskId: task_id,
|
|
177
|
+
message: `Thread ${thread_id} attached to task ${task_id}`,
|
|
178
|
+
});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return JSON.stringify({ error: err.message || String(err) });
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
});
|
package/unify/web-bridge.js
CHANGED
|
@@ -29,9 +29,6 @@ const QUERY_TIMEOUT_MS = 120_000;
|
|
|
29
29
|
/** Virtual conversationId for the Unify session */
|
|
30
30
|
let unifyConversationId = null;
|
|
31
31
|
|
|
32
|
-
/** Current query mode: 'chat' or 'work' */
|
|
33
|
-
let currentMode = 'chat';
|
|
34
|
-
|
|
35
32
|
/** Accumulated conversation messages for context continuity across queries.
|
|
36
33
|
* Each entry is { role: 'user'|'assistant', content: string|Array }.
|
|
37
34
|
* Cleared on session reset or consolidation. */
|
|
@@ -79,14 +76,16 @@ function sendUnifyEvent(event) {
|
|
|
79
76
|
* Handle a unify_chat message from the web UI.
|
|
80
77
|
*
|
|
81
78
|
* @param {{ prompt: string, mode?: string, userId?: string, username?: string }} msg
|
|
79
|
+
* NOTE: `mode` is deprecated (task-297) — Unify now runs in a single unified mode.
|
|
80
|
+
* If present, a warning is logged and the field is ignored.
|
|
82
81
|
*/
|
|
83
82
|
export async function handleUnifyChat(msg) {
|
|
84
83
|
const { prompt, mode } = msg;
|
|
85
84
|
if (!prompt?.trim()) return;
|
|
86
85
|
|
|
87
|
-
//
|
|
88
|
-
if (mode
|
|
89
|
-
|
|
86
|
+
// Deprecation warning — task-297 removed chat/work mode distinction
|
|
87
|
+
if (mode !== undefined && mode !== null) {
|
|
88
|
+
console.warn('[Unify] unify_chat.mode is deprecated and ignored — Unify now runs in a single unified mode.');
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
try {
|
|
@@ -150,7 +149,6 @@ export async function handleUnifyChat(msg) {
|
|
|
150
149
|
// ─── Stream Engine events → claude_output format ──
|
|
151
150
|
for await (const event of session.engine.query({
|
|
152
151
|
prompt,
|
|
153
|
-
mode: currentMode,
|
|
154
152
|
messages: conversationMessages,
|
|
155
153
|
signal: currentAbort.signal,
|
|
156
154
|
})) {
|
|
@@ -192,12 +190,15 @@ export async function handleUnifyChat(msg) {
|
|
|
192
190
|
input: event.input,
|
|
193
191
|
}],
|
|
194
192
|
},
|
|
193
|
+
threadId: event.threadId,
|
|
195
194
|
});
|
|
196
195
|
break;
|
|
197
196
|
|
|
198
197
|
// ── Tool execution started ──
|
|
199
198
|
case 'tool_start':
|
|
200
|
-
// Tool is running — the UI already shows it from tool_use block above
|
|
199
|
+
// Tool is running — the UI already shows it from tool_use block above.
|
|
200
|
+
// Forward threadId so the UI can group tool activity by thread (Phase 1).
|
|
201
|
+
sendUnifyEvent({ type: 'tool_start', id: event.id, name: event.name, threadId: event.threadId });
|
|
201
202
|
break;
|
|
202
203
|
|
|
203
204
|
// ── Tool execution completed ──
|
|
@@ -211,6 +212,7 @@ export async function handleUnifyChat(msg) {
|
|
|
211
212
|
content: event.output || '',
|
|
212
213
|
is_error: event.isError || false,
|
|
213
214
|
}],
|
|
215
|
+
threadId: event.threadId,
|
|
214
216
|
});
|
|
215
217
|
break;
|
|
216
218
|
|
|
@@ -406,12 +408,12 @@ export async function handleUnifyChat(msg) {
|
|
|
406
408
|
|
|
407
409
|
/**
|
|
408
410
|
* Handle mode switch from the web UI.
|
|
409
|
-
*
|
|
411
|
+
* DEPRECATED (task-297): Unify no longer has chat/work mode distinction.
|
|
412
|
+
* Retained as a no-op with warning for backward compatibility.
|
|
413
|
+
* @param {{ mode?: string }} _msg
|
|
410
414
|
*/
|
|
411
|
-
export function handleUnifyModeSwitch(
|
|
412
|
-
|
|
413
|
-
currentMode = msg.mode;
|
|
414
|
-
}
|
|
415
|
+
export function handleUnifyModeSwitch(_msg) {
|
|
416
|
+
console.warn('[Unify] unify_mode_switch is deprecated and ignored — Unify now runs in a single unified mode.');
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
/**
|
|
@@ -518,7 +520,6 @@ export async function resetUnifySession() {
|
|
|
518
520
|
session = null;
|
|
519
521
|
}
|
|
520
522
|
unifyConversationId = null;
|
|
521
|
-
currentMode = 'chat';
|
|
522
523
|
conversationMessages = [];
|
|
523
524
|
|
|
524
525
|
// Re-initialize session immediately so frontend gets updated config
|