@yeaft/webchat-agent 0.1.587 → 0.1.590
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/connection/message-router.js +18 -1
- package/package.json +1 -1
- package/unify/tools/task-tools.js +112 -6
- package/unify/tools/types.js +5 -0
- package/unify/vp/run-turn.js +54 -0
- package/unify/vp/system-prompt.js +109 -0
- package/unify/web-bridge.js +273 -0
|
@@ -36,7 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
38
|
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings } from '../unify/config-api.js';
|
|
39
|
-
import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove, handleUnifyMemoryScopeList, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
39
|
+
import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove, handleUnifyMemoryScopeList, handleUnifyMemoryQuery, handleUnifyMemoryTrace, handleUnifyFetchSummaryHistory, handleUnifyTaskCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -446,6 +446,23 @@ export async function handleMessage(msg) {
|
|
|
446
446
|
handleUnifyMemoryScopeList(msg);
|
|
447
447
|
break;
|
|
448
448
|
|
|
449
|
+
// R6 G2 — VP/Task memory browser (read-only). Replies with
|
|
450
|
+
// unify_memory_query_result / unify_memory_trace_result.
|
|
451
|
+
case 'unify_memory_query':
|
|
452
|
+
handleUnifyMemoryQuery(msg);
|
|
453
|
+
break;
|
|
454
|
+
case 'unify_memory_trace':
|
|
455
|
+
handleUnifyMemoryTrace(msg);
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
// R6 G1a — task summary history + task affiliation CRUD.
|
|
459
|
+
case 'unify_fetch_summary_history':
|
|
460
|
+
await handleUnifyFetchSummaryHistory(msg);
|
|
461
|
+
break;
|
|
462
|
+
case 'unify_task_crud':
|
|
463
|
+
await handleUnifyTaskCrud(msg);
|
|
464
|
+
break;
|
|
465
|
+
|
|
449
466
|
// task-334m: Group CRUD + D1 seed wiring (§Δ10 334m + R6 §Δ31.2).
|
|
450
467
|
// All handlers reply via `group_crud_result`; mutating ops additionally
|
|
451
468
|
// emit `group_roster_changed` (add/remove/default) or
|
package/package.json
CHANGED
|
@@ -50,7 +50,15 @@ Tasks have a title, description, priority, and status.
|
|
|
50
50
|
Each task gets its own folder with task.md, progress.md, and memory.md.
|
|
51
51
|
Use this to break down complex work into trackable items.
|
|
52
52
|
|
|
53
|
-
Pass \`parent_id\` to create a subtask under an existing task
|
|
53
|
+
Pass \`parent_id\` to create a subtask under an existing task.
|
|
54
|
+
|
|
55
|
+
R6 multi-VP groups (Unify): pass \`group_id\` + \`members\` to create a
|
|
56
|
+
collaborative task inside a group. The caller's vpId becomes the task
|
|
57
|
+
\`initiator\`. \`members\` MUST be a subset of the group's roster — the
|
|
58
|
+
tool validates this server-side and returns a \`not_in_roster\` error
|
|
59
|
+
otherwise. The user owns invitations; the tool will not auto-invite.
|
|
60
|
+
|
|
61
|
+
Use \`related_task_ids\` to soft-link to other tasks (cross-group OK).`,
|
|
54
62
|
parameters: {
|
|
55
63
|
type: 'object',
|
|
56
64
|
properties: {
|
|
@@ -71,6 +79,20 @@ Pass \`parent_id\` to create a subtask under an existing task.`,
|
|
|
71
79
|
type: 'string',
|
|
72
80
|
description: 'Parent task ID for subtasks',
|
|
73
81
|
},
|
|
82
|
+
group_id: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'R6: group this task belongs to. Required for multi-VP collaboration tasks.',
|
|
85
|
+
},
|
|
86
|
+
members: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
items: { type: 'string' },
|
|
89
|
+
description: 'R6: VP ids participating in this task (≥1). MUST be ⊆ group roster.',
|
|
90
|
+
},
|
|
91
|
+
related_task_ids: {
|
|
92
|
+
type: 'array',
|
|
93
|
+
items: { type: 'string' },
|
|
94
|
+
description: 'R6: soft-linked task ids (cross-group OK). See arch §14.',
|
|
95
|
+
},
|
|
74
96
|
// Note (task-333b): `parent_task_id` is accepted by execute() as a
|
|
75
97
|
// soft-compat alias for `parent_id` (absorbed from the former
|
|
76
98
|
// SpawnTask tool) but intentionally NOT advertised in the schema to
|
|
@@ -84,7 +106,16 @@ Pass \`parent_id\` to create a subtask under an existing task.`,
|
|
|
84
106
|
const err = requireStore();
|
|
85
107
|
if (err) return err;
|
|
86
108
|
|
|
87
|
-
const {
|
|
109
|
+
const {
|
|
110
|
+
title,
|
|
111
|
+
description,
|
|
112
|
+
priority = 'medium',
|
|
113
|
+
parent_id,
|
|
114
|
+
parent_task_id,
|
|
115
|
+
group_id,
|
|
116
|
+
members,
|
|
117
|
+
related_task_ids,
|
|
118
|
+
} = input;
|
|
88
119
|
if (!title) return JSON.stringify({ error: 'title is required' });
|
|
89
120
|
|
|
90
121
|
// task-333b: accept either `parent_id` (original TaskCreate field) or
|
|
@@ -95,6 +126,62 @@ Pass \`parent_id\` to create a subtask under an existing task.`,
|
|
|
95
126
|
return JSON.stringify({ error: `Parent task not found: ${parentId}` });
|
|
96
127
|
}
|
|
97
128
|
|
|
129
|
+
// R6 multi-VP fields — validated only when group_id is present, so
|
|
130
|
+
// legacy single-tenant TaskCreate calls keep working.
|
|
131
|
+
let groupId = null;
|
|
132
|
+
let normalizedMembers = null;
|
|
133
|
+
let initiator = null;
|
|
134
|
+
if (group_id) {
|
|
135
|
+
groupId = String(group_id);
|
|
136
|
+
|
|
137
|
+
// Validate members ⊆ roster. We resolve the roster via the tool ctx
|
|
138
|
+
// because the tool layer must not import group-store directly (loose
|
|
139
|
+
// coupling — ctx.getGroupRoster is wired in session.js).
|
|
140
|
+
let roster = null;
|
|
141
|
+
if (typeof ctx?.getGroupRoster === 'function') {
|
|
142
|
+
try { roster = ctx.getGroupRoster(groupId); } catch { roster = null; }
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(roster)) {
|
|
145
|
+
return JSON.stringify({
|
|
146
|
+
error: 'group_not_found',
|
|
147
|
+
hint: `group ${groupId} has no roster (group not loaded or doesn't exist)`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Default members to [initiator] if not given (R6 §1.5: members ≥ 1).
|
|
152
|
+
const callerVpId = ctx?.currentVpId || null;
|
|
153
|
+
const candidateMembers = Array.isArray(members) && members.length > 0
|
|
154
|
+
? members.map(String)
|
|
155
|
+
: (callerVpId ? [callerVpId] : []);
|
|
156
|
+
if (candidateMembers.length === 0) {
|
|
157
|
+
return JSON.stringify({
|
|
158
|
+
error: 'no_members',
|
|
159
|
+
hint: 'Specify members[] (≥1) or call from a VP context (currentVpId resolves to self).',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const offRoster = candidateMembers.filter((m) => !roster.includes(m));
|
|
163
|
+
if (offRoster.length > 0) {
|
|
164
|
+
return JSON.stringify({
|
|
165
|
+
error: 'not_in_roster',
|
|
166
|
+
offRoster,
|
|
167
|
+
roster,
|
|
168
|
+
hint: 'These VP ids are not in the group roster. Ask the user to invite them first; do not auto-invite.',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Always include the caller as a member (initiator must be ∈ members).
|
|
172
|
+
if (callerVpId && !candidateMembers.includes(callerVpId)) {
|
|
173
|
+
if (!roster.includes(callerVpId)) {
|
|
174
|
+
return JSON.stringify({
|
|
175
|
+
error: 'caller_not_in_roster',
|
|
176
|
+
hint: `caller VP ${callerVpId} is not in group ${groupId} roster.`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
candidateMembers.unshift(callerVpId);
|
|
180
|
+
}
|
|
181
|
+
normalizedMembers = Array.from(new Set(candidateMembers));
|
|
182
|
+
initiator = callerVpId;
|
|
183
|
+
}
|
|
184
|
+
|
|
98
185
|
const id = `task-${randomUUID().slice(0, 8)}`;
|
|
99
186
|
const task = {
|
|
100
187
|
id,
|
|
@@ -107,15 +194,34 @@ Pass \`parent_id\` to create a subtask under an existing task.`,
|
|
|
107
194
|
createdAt: Date.now(),
|
|
108
195
|
updatedAt: Date.now(),
|
|
109
196
|
};
|
|
197
|
+
if (groupId) {
|
|
198
|
+
task.groupId = groupId;
|
|
199
|
+
task.members = normalizedMembers;
|
|
200
|
+
if (initiator) task.initiator = initiator;
|
|
201
|
+
if (Array.isArray(related_task_ids) && related_task_ids.length) {
|
|
202
|
+
task.relatedTaskIds = related_task_ids.map(String);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
110
205
|
|
|
111
206
|
taskStore.create(task);
|
|
112
207
|
|
|
113
208
|
return JSON.stringify({
|
|
114
209
|
success: true,
|
|
115
|
-
task: {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
210
|
+
task: {
|
|
211
|
+
id,
|
|
212
|
+
title,
|
|
213
|
+
priority,
|
|
214
|
+
status: 'pending',
|
|
215
|
+
parentTaskId: parentId,
|
|
216
|
+
groupId: groupId || undefined,
|
|
217
|
+
members: normalizedMembers || undefined,
|
|
218
|
+
initiator: initiator || undefined,
|
|
219
|
+
},
|
|
220
|
+
message: groupId
|
|
221
|
+
? `Task created in group ${groupId}: ${title} (${id}) with members [${(normalizedMembers || []).join(', ')}]`
|
|
222
|
+
: parentId
|
|
223
|
+
? `Subtask created: ${title} (${id}) under ${parentId}`
|
|
224
|
+
: `Task created: ${title} (${id})`,
|
|
119
225
|
});
|
|
120
226
|
},
|
|
121
227
|
});
|
package/unify/tools/types.js
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* @property {object} [skillManager] — Skill manager
|
|
20
20
|
* @property {object} [trace] — debug trace
|
|
21
21
|
* @property {object} [config] — engine config
|
|
22
|
+
* @property {string} [currentVpId] — R6: VP id of the caller (set in multi-VP groups)
|
|
23
|
+
* @property {string} [currentGroupId] — R6: group id of the caller's RoleInstance
|
|
24
|
+
* @property {(groupId: string) => string[]|null} [getGroupRoster]
|
|
25
|
+
* — R6: resolve a group's roster (used by TaskCreate / route_forward to
|
|
26
|
+
* validate `members` ⊆ roster without importing group-store directly).
|
|
22
27
|
*/
|
|
23
28
|
|
|
24
29
|
/**
|
package/unify/vp/run-turn.js
CHANGED
|
@@ -28,6 +28,53 @@
|
|
|
28
28
|
|
|
29
29
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Build the taskCtx opt for buildSystemPrompt. Pulls active tasks for the
|
|
33
|
+
* VP's current group from the task store, plus the current task (if any).
|
|
34
|
+
* Returns null if no taskStore is wired (legacy / tests).
|
|
35
|
+
*/
|
|
36
|
+
function collectTaskCtx({ taskStore, groupId, currentTaskId }) {
|
|
37
|
+
if (!taskStore || !groupId) return null;
|
|
38
|
+
let allTasks;
|
|
39
|
+
try {
|
|
40
|
+
allTasks = taskStore.list();
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(allTasks)) return null;
|
|
45
|
+
|
|
46
|
+
const inGroup = allTasks.filter(
|
|
47
|
+
(t) => t && t.groupId === groupId && t.status !== 'completed' && t.status !== 'cancelled',
|
|
48
|
+
);
|
|
49
|
+
// Order by lastActivity / updatedAt desc so most-recent surface first.
|
|
50
|
+
inGroup.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
51
|
+
|
|
52
|
+
const activeTasks = inGroup.map((t) => ({
|
|
53
|
+
id: t.id,
|
|
54
|
+
title: t.title,
|
|
55
|
+
status: t.status,
|
|
56
|
+
members: Array.isArray(t.members) ? t.members.slice() : [],
|
|
57
|
+
initiator: t.initiator || null,
|
|
58
|
+
lastActivityAt: t.updatedAt || t.createdAt || 0,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
let currentTask = null;
|
|
62
|
+
if (currentTaskId) {
|
|
63
|
+
const t = allTasks.find((x) => x && x.id === currentTaskId);
|
|
64
|
+
if (t) {
|
|
65
|
+
currentTask = {
|
|
66
|
+
id: t.id,
|
|
67
|
+
title: t.title,
|
|
68
|
+
members: Array.isArray(t.members) ? t.members.slice() : [],
|
|
69
|
+
initiator: t.initiator || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (activeTasks.length === 0 && !currentTask) return null;
|
|
75
|
+
return { activeTasks, currentTask };
|
|
76
|
+
}
|
|
77
|
+
|
|
31
78
|
/**
|
|
32
79
|
* Build a runner suitable for RoleInstance.drain().
|
|
33
80
|
*
|
|
@@ -50,6 +97,7 @@ export function createTurnRunner(deps = {}) {
|
|
|
50
97
|
capabilitiesLine,
|
|
51
98
|
onEvent,
|
|
52
99
|
buildPromptOverride,
|
|
100
|
+
taskStore, // R6 §6 trigger #6: enables task_ctx affiliation hint
|
|
53
101
|
} = deps;
|
|
54
102
|
|
|
55
103
|
if (!binder || typeof binder.bind !== 'function') {
|
|
@@ -72,6 +120,11 @@ export function createTurnRunner(deps = {}) {
|
|
|
72
120
|
const engine = binder.bind(ri);
|
|
73
121
|
|
|
74
122
|
// Fresh system prompt per turn — DYNAMIC section changes every turn.
|
|
123
|
+
const taskCtx = collectTaskCtx({
|
|
124
|
+
taskStore,
|
|
125
|
+
groupId: ri.groupId,
|
|
126
|
+
currentTaskId: envelope.taskId || null,
|
|
127
|
+
});
|
|
75
128
|
const systemPrompt = await buildPrompt(ri, {
|
|
76
129
|
registry,
|
|
77
130
|
rosterMembers,
|
|
@@ -80,6 +133,7 @@ export function createTurnRunner(deps = {}) {
|
|
|
80
133
|
taskId: envelope.taskId || null,
|
|
81
134
|
isDream: false,
|
|
82
135
|
},
|
|
136
|
+
taskCtx,
|
|
83
137
|
});
|
|
84
138
|
|
|
85
139
|
// Prompt text = inbound message body. Engine.query spec:
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* § DYNAMIC
|
|
17
17
|
* - runtime_ctx { vpId, groupId, taskId?, isDream=false }
|
|
18
18
|
* - core_memory (recall top-K; vp single-dim per R3 §Δ2.3)
|
|
19
|
+
* - task_ctx (R6 §8: lists active tasks in current group + affiliation
|
|
20
|
+
* hint guiding the LLM to call task_create / continue an
|
|
21
|
+
* existing task / reply at group level — this is the
|
|
22
|
+
* mechanism that lets tasks "auto-emerge" from chat per
|
|
23
|
+
* R6 §6 trigger #6.)
|
|
19
24
|
*
|
|
20
25
|
* Caching: the STATIC persona section is cached per RoleInstance via
|
|
21
26
|
* `ri._promptBuiltForMtime === vp.mtimeMs`. DYNAMIC is always rebuilt.
|
|
@@ -42,6 +47,17 @@ const ROSTER_STATUS_BUSY = 'busy';
|
|
|
42
47
|
* capabilitiesLine?: string, // 334d injects tool inventory
|
|
43
48
|
* userProfile?: string, // 334l injects top-5 user-memory
|
|
44
49
|
* recentGroupChat?: string, // 334h injects N recent msgs
|
|
50
|
+
* taskCtx?: {
|
|
51
|
+
* activeTasks?: Array<{
|
|
52
|
+
* id: string, title?: string, status?: string,
|
|
53
|
+
* members?: string[], initiator?: string,
|
|
54
|
+
* lastActivityAt?: number,
|
|
55
|
+
* }>, // tasks in this group the VP can join/continue
|
|
56
|
+
* currentTask?: { // the task the VP is currently inside
|
|
57
|
+
* id: string, title?: string, members?: string[], initiator?: string,
|
|
58
|
+
* },
|
|
59
|
+
* relatedTasks?: Array<{ id: string, title?: string, groupId?: string }>,
|
|
60
|
+
* },
|
|
45
61
|
* }} opts
|
|
46
62
|
* @returns {string}
|
|
47
63
|
*/
|
|
@@ -68,6 +84,7 @@ export async function buildSystemPrompt(ri, opts = {}) {
|
|
|
68
84
|
? `\n## recent_group_chat\n${opts.recentGroupChat.trim()}\n`
|
|
69
85
|
: '';
|
|
70
86
|
const coreMem = await buildCoreMemoryBlock(ri, ctx);
|
|
87
|
+
const taskCtx = buildTaskCtxBlock(ri, opts.taskCtx, ctx);
|
|
71
88
|
|
|
72
89
|
return [
|
|
73
90
|
'# § STATIC',
|
|
@@ -81,6 +98,7 @@ export async function buildSystemPrompt(ri, opts = {}) {
|
|
|
81
98
|
runtime,
|
|
82
99
|
recent.trim() ? recent : '',
|
|
83
100
|
coreMem,
|
|
101
|
+
taskCtx,
|
|
84
102
|
]
|
|
85
103
|
.filter(Boolean)
|
|
86
104
|
.join('\n')
|
|
@@ -189,3 +207,94 @@ async function buildCoreMemoryBlock(ri, ctx) {
|
|
|
189
207
|
if (lines.length === 1) return '';
|
|
190
208
|
return lines.join('\n');
|
|
191
209
|
}
|
|
210
|
+
|
|
211
|
+
// ──────────────────────────────────────────────────────────────
|
|
212
|
+
// DYNAMIC — task_ctx (R6 §6 trigger #6 + §8)
|
|
213
|
+
//
|
|
214
|
+
// The affiliation hint is the load-bearing piece that lets tasks
|
|
215
|
+
// auto-emerge from chat. Without it, VPs never call task_create —
|
|
216
|
+
// the spec lists the tool but a model with no nudge will just keep
|
|
217
|
+
// chatting at group level. We list active tasks in the group + give
|
|
218
|
+
// crisp guidance on when to (a) continue an existing task, (b) start
|
|
219
|
+
// a new one, or (c) reply at group level.
|
|
220
|
+
// ──────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
function buildTaskCtxBlock(ri, taskCtx, runtimeCtx) {
|
|
223
|
+
if (!taskCtx) return '';
|
|
224
|
+
|
|
225
|
+
const currentTask = taskCtx.currentTask;
|
|
226
|
+
const activeTasks = Array.isArray(taskCtx.activeTasks) ? taskCtx.activeTasks : [];
|
|
227
|
+
const relatedTasks = Array.isArray(taskCtx.relatedTasks) ? taskCtx.relatedTasks : [];
|
|
228
|
+
|
|
229
|
+
const otherTasks = activeTasks
|
|
230
|
+
.filter((t) => t && t.id && (!currentTask || t.id !== currentTask.id))
|
|
231
|
+
.slice(0, 8); // cap to avoid prompt bloat — coordinator orders by recency
|
|
232
|
+
|
|
233
|
+
// Nothing useful to say — drop the block entirely. Without active tasks,
|
|
234
|
+
// a current task, or related tasks, the affiliation_hint has no anchor.
|
|
235
|
+
if (otherTasks.length === 0 && !currentTask && relatedTasks.length === 0) {
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lines = ['## task_ctx'];
|
|
240
|
+
|
|
241
|
+
// (a) currently inside a task → focus on this one
|
|
242
|
+
if (currentTask && currentTask.id) {
|
|
243
|
+
lines.push(`### current_task`);
|
|
244
|
+
lines.push(`- id: ${currentTask.id}`);
|
|
245
|
+
if (currentTask.title) lines.push(`- title: ${currentTask.title}`);
|
|
246
|
+
if (currentTask.initiator) lines.push(`- initiator: ${currentTask.initiator}`);
|
|
247
|
+
if (Array.isArray(currentTask.members) && currentTask.members.length) {
|
|
248
|
+
lines.push(`- members: ${currentTask.members.join(', ')}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// (b) other open tasks in the same group — candidates for affiliation
|
|
253
|
+
if (otherTasks.length > 0) {
|
|
254
|
+
lines.push(`### active_tasks_in_group (${otherTasks.length})`);
|
|
255
|
+
for (const t of otherTasks) {
|
|
256
|
+
const parts = [`- ${t.id}`];
|
|
257
|
+
if (t.title) parts.push(`"${t.title}"`);
|
|
258
|
+
if (t.status) parts.push(`(${t.status})`);
|
|
259
|
+
if (Array.isArray(t.members) && t.members.length) {
|
|
260
|
+
parts.push(`members=[${t.members.join(',')}]`);
|
|
261
|
+
}
|
|
262
|
+
lines.push(parts.join(' '));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// (c) related tasks (recall top-N from §14)
|
|
267
|
+
if (relatedTasks.length > 0) {
|
|
268
|
+
lines.push(`### related_tasks (${relatedTasks.length})`);
|
|
269
|
+
for (const t of relatedTasks.slice(0, 3)) {
|
|
270
|
+
const parts = [`- ${t.id}`];
|
|
271
|
+
if (t.title) parts.push(`"${t.title}"`);
|
|
272
|
+
if (t.groupId && t.groupId !== ri.groupId) parts.push(`(grp:${t.groupId})`);
|
|
273
|
+
lines.push(parts.join(' '));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// (d) affiliation hint — the actual decision rubric
|
|
278
|
+
// Suppress full hint when already inside a task (focus on continuing).
|
|
279
|
+
const inTask = Boolean(runtimeCtx?.taskId || currentTask?.id);
|
|
280
|
+
if (!inTask) {
|
|
281
|
+
lines.push('');
|
|
282
|
+
lines.push('### affiliation_hint');
|
|
283
|
+
lines.push('Decide BEFORE replying:');
|
|
284
|
+
lines.push('- If this user message clearly continues one of the active_tasks_in_group above,');
|
|
285
|
+
lines.push(' use the `task_message` route (other VPs in that task will see your reply, others will not).');
|
|
286
|
+
lines.push('- If this is a NEW multi-turn collaboration worth tracking (decisions, deliverables,');
|
|
287
|
+
lines.push(' multiple VPs needed), call `TaskCreate` with title + members[] + groupId; you become');
|
|
288
|
+
lines.push(' the task initiator and may post `task_summary_post` to broadcast progress.');
|
|
289
|
+
lines.push('- If this is just a quick question / chitchat / single-shot answer, reply at group level — no task.');
|
|
290
|
+
lines.push('Do NOT create a task for trivial back-and-forth. Do NOT continue a task whose topic clearly diverged.');
|
|
291
|
+
} else {
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push('### affiliation_hint');
|
|
294
|
+
lines.push('You are inside a task. Stay focused on its scope. Use `task_summary_post` (initiator only)');
|
|
295
|
+
lines.push('to broadcast progress to the group. Use `route_forward` to hand off; do not silently leave.');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (lines.length === 1) return '';
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
package/unify/web-bridge.js
CHANGED
|
@@ -1635,6 +1635,279 @@ export function handleUnifyModeSwitch(_msg) {
|
|
|
1635
1635
|
console.warn('[Unify] unify_mode_switch is deprecated and ignored — Unify now runs in a single unified mode.');
|
|
1636
1636
|
}
|
|
1637
1637
|
|
|
1638
|
+
/**
|
|
1639
|
+
* R6 G2 — VP/Task memory browser query.
|
|
1640
|
+
*
|
|
1641
|
+
* Reads from session.memoryShardStore (R6 shard-based memory) and replies
|
|
1642
|
+
* with a time-sorted list of entries scoped to the requested vpId / taskId.
|
|
1643
|
+
* The web UI's MemoryCard / MemoryTraceModal consume the reply.
|
|
1644
|
+
*
|
|
1645
|
+
* Request shape:
|
|
1646
|
+
* { type: 'unify_memory_query',
|
|
1647
|
+
* vpId?: string, taskId?: string,
|
|
1648
|
+
* limit?: number, requestId?: string }
|
|
1649
|
+
*
|
|
1650
|
+
* Reply shape:
|
|
1651
|
+
* { type: 'unify_memory_query_result',
|
|
1652
|
+
* scope: { vpId, taskId },
|
|
1653
|
+
* entries: Array<thinEntry>, // shape from shard-store mapRecordToThinEntry
|
|
1654
|
+
* requestId? }
|
|
1655
|
+
*
|
|
1656
|
+
* Per D2 the query is scoped — the LLM owns memory recall via the
|
|
1657
|
+
* memory_query tool; this surface is purely UI browsing (read-only).
|
|
1658
|
+
*
|
|
1659
|
+
* @param {{ vpId?: string, taskId?: string, limit?: number, requestId?: string }} msg
|
|
1660
|
+
*/
|
|
1661
|
+
export function handleUnifyMemoryQuery(msg = {}) {
|
|
1662
|
+
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
1663
|
+
const vpId = typeof msg.vpId === 'string' ? msg.vpId : null;
|
|
1664
|
+
const taskId = typeof msg.taskId === 'string' ? msg.taskId : null;
|
|
1665
|
+
const limit = Number.isFinite(msg.limit) ? Math.max(1, Math.min(200, msg.limit)) : 50;
|
|
1666
|
+
|
|
1667
|
+
const reply = (extra = {}) => sendUnifyEvent({
|
|
1668
|
+
type: 'unify_memory_query_result',
|
|
1669
|
+
scope: { vpId, taskId },
|
|
1670
|
+
...extra,
|
|
1671
|
+
...(requestId ? { requestId } : {}),
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
if (!session || !session.memoryShardStore) {
|
|
1675
|
+
reply({ entries: [], error: 'no_memory_store' });
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
try {
|
|
1680
|
+
const filter = {};
|
|
1681
|
+
if (vpId) filter.vp = vpId;
|
|
1682
|
+
if (taskId) filter.task = taskId;
|
|
1683
|
+
const res = session.memoryShardStore.query(filter);
|
|
1684
|
+
const list = Array.isArray(res?.results) ? res.results : [];
|
|
1685
|
+
// Time-sorted desc on updatedAt / createdAt.
|
|
1686
|
+
list.sort((a, b) => {
|
|
1687
|
+
const ax = (a && (a.updatedAt || a.createdAt)) || 0;
|
|
1688
|
+
const bx = (b && (b.updatedAt || b.createdAt)) || 0;
|
|
1689
|
+
const at = typeof ax === 'string' ? Date.parse(ax) : ax;
|
|
1690
|
+
const bt = typeof bx === 'string' ? Date.parse(bx) : bx;
|
|
1691
|
+
return (bt || 0) - (at || 0);
|
|
1692
|
+
});
|
|
1693
|
+
reply({ entries: list.slice(0, limit) });
|
|
1694
|
+
} catch (err) {
|
|
1695
|
+
reply({ entries: [], error: String(err?.message || err) });
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* R6 G2 — Open the source message behind a memory entry (memory_trace).
|
|
1701
|
+
*
|
|
1702
|
+
* Resolves entry → sourceRef.{conversationId, messageId} (or threadId/range)
|
|
1703
|
+
* via MemoryShardStore.get(entryId), then echoes the reference + the entry
|
|
1704
|
+
* for the MemoryTraceModal to render. The trace itself is a read-only
|
|
1705
|
+
* surface — the UI follows the conversationId/messageId to MessageList.
|
|
1706
|
+
*
|
|
1707
|
+
* Request shape:
|
|
1708
|
+
* { type: 'unify_memory_trace', entryId: string, requestId?: string }
|
|
1709
|
+
*
|
|
1710
|
+
* Reply shape:
|
|
1711
|
+
* { type: 'unify_memory_trace_result',
|
|
1712
|
+
* entryId, entry: object|null, sourceRef: object|null, requestId? }
|
|
1713
|
+
*
|
|
1714
|
+
* @param {{ entryId?: string, requestId?: string }} msg
|
|
1715
|
+
*/
|
|
1716
|
+
export function handleUnifyMemoryTrace(msg = {}) {
|
|
1717
|
+
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
1718
|
+
const entryId = typeof msg.entryId === 'string' ? msg.entryId : null;
|
|
1719
|
+
|
|
1720
|
+
const reply = (extra = {}) => sendUnifyEvent({
|
|
1721
|
+
type: 'unify_memory_trace_result',
|
|
1722
|
+
entryId,
|
|
1723
|
+
...extra,
|
|
1724
|
+
...(requestId ? { requestId } : {}),
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
if (!entryId) { reply({ entry: null, sourceRef: null, error: 'missing_entry_id' }); return; }
|
|
1728
|
+
if (!session || !session.memoryShardStore) {
|
|
1729
|
+
reply({ entry: null, sourceRef: null, error: 'no_memory_store' });
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
try {
|
|
1733
|
+
const entry = session.memoryShardStore.get(entryId);
|
|
1734
|
+
if (!entry) { reply({ entry: null, sourceRef: null, error: 'not_found' }); return; }
|
|
1735
|
+
reply({ entry, sourceRef: entry.sourceRef || null });
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
reply({ entry: null, sourceRef: null, error: String(err?.message || err) });
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* R6 G1a — Fetch a task's summary history (revision chain).
|
|
1743
|
+
*
|
|
1744
|
+
* Streams the group log filtered by `taskId` and `meta.kind === 'summary'`,
|
|
1745
|
+
* separates `current` (≤10 most recent non-superseded) from `archived` rows
|
|
1746
|
+
* per §Δ31.5. Default `includeArchived: false` keeps the wire payload small;
|
|
1747
|
+
* the UI's "Show archived" button re-issues with the flag set.
|
|
1748
|
+
*
|
|
1749
|
+
* Request shape:
|
|
1750
|
+
* { type: 'unify_fetch_summary_history', taskId, includeArchived?: bool }
|
|
1751
|
+
*
|
|
1752
|
+
* Reply shape:
|
|
1753
|
+
* { type: 'unify_summary_history', taskId, revisions: [...],
|
|
1754
|
+
* archived: [...]|null, error?: string, requestId? }
|
|
1755
|
+
*/
|
|
1756
|
+
export async function handleUnifyFetchSummaryHistory(msg = {}) {
|
|
1757
|
+
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
1758
|
+
const taskId = typeof msg.taskId === 'string' ? msg.taskId : null;
|
|
1759
|
+
const includeArchived = !!msg.includeArchived;
|
|
1760
|
+
|
|
1761
|
+
const reply = (extra = {}) => sendUnifyEvent({
|
|
1762
|
+
type: 'unify_summary_history',
|
|
1763
|
+
taskId,
|
|
1764
|
+
...extra,
|
|
1765
|
+
...(requestId ? { requestId } : {}),
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
if (!taskId) { reply({ revisions: [], archived: null, error: 'missing_task_id' }); return; }
|
|
1769
|
+
|
|
1770
|
+
try {
|
|
1771
|
+
const { getTaskStore } = await import('./tools/task-tools.js');
|
|
1772
|
+
const taskStore = getTaskStore();
|
|
1773
|
+
const task = taskStore?.get(taskId);
|
|
1774
|
+
if (!task) { reply({ revisions: [], archived: null, error: 'task_not_found' }); return; }
|
|
1775
|
+
const groupId = task.groupId;
|
|
1776
|
+
if (!groupId) { reply({ revisions: [], archived: null, error: 'task_has_no_group' }); return; }
|
|
1777
|
+
|
|
1778
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1779
|
+
if (!yeaftDir) { reply({ revisions: [], archived: null, error: 'no_yeaft_dir' }); return; }
|
|
1780
|
+
|
|
1781
|
+
const { openGroup, loadGroupMeta } = await import('./groups/group-store.js');
|
|
1782
|
+
const { join } = await import('node:path');
|
|
1783
|
+
const { existsSync } = await import('node:fs');
|
|
1784
|
+
const root = join(yeaftDir, 'groups');
|
|
1785
|
+
const dir = join(root, groupId);
|
|
1786
|
+
if (!existsSync(dir) || !loadGroupMeta(dir)) {
|
|
1787
|
+
reply({ revisions: [], archived: null, error: 'group_not_found' });
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
const groupHandle = openGroup(root, groupId);
|
|
1791
|
+
const summaries = [];
|
|
1792
|
+
for (const m of groupHandle.streamMessages()) {
|
|
1793
|
+
if (!m || m.taskId !== taskId) continue;
|
|
1794
|
+
const meta = m.meta || {};
|
|
1795
|
+
if (meta.kind === 'summary' || meta.type === 'summary') summaries.push(m);
|
|
1796
|
+
}
|
|
1797
|
+
summaries.sort((a, b) => {
|
|
1798
|
+
const at = Date.parse(a.ts || '') || 0;
|
|
1799
|
+
const bt = Date.parse(b.ts || '') || 0;
|
|
1800
|
+
return bt - at;
|
|
1801
|
+
});
|
|
1802
|
+
const supersededIds = new Set();
|
|
1803
|
+
for (const s of summaries) {
|
|
1804
|
+
const arr = s.meta?.supersedes;
|
|
1805
|
+
if (Array.isArray(arr)) for (const id of arr) supersededIds.add(id);
|
|
1806
|
+
}
|
|
1807
|
+
const current = [];
|
|
1808
|
+
const archived = [];
|
|
1809
|
+
for (const s of summaries) {
|
|
1810
|
+
if (supersededIds.has(s.id)) archived.push(s);
|
|
1811
|
+
else current.push(s);
|
|
1812
|
+
}
|
|
1813
|
+
// §Δ31.5: keep only 10 in current; oldest extras spill to archived.
|
|
1814
|
+
const overflow = current.slice(10);
|
|
1815
|
+
const trimmedCurrent = current.slice(0, 10);
|
|
1816
|
+
if (overflow.length) archived.push(...overflow);
|
|
1817
|
+
archived.sort((a, b) => {
|
|
1818
|
+
const at = Date.parse(a.ts || '') || 0;
|
|
1819
|
+
const bt = Date.parse(b.ts || '') || 0;
|
|
1820
|
+
return bt - at;
|
|
1821
|
+
});
|
|
1822
|
+
reply({
|
|
1823
|
+
revisions: trimmedCurrent,
|
|
1824
|
+
archived: includeArchived ? archived : null,
|
|
1825
|
+
});
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
reply({ revisions: [], archived: null, error: String(err?.message || err) });
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* R6 G1a — Task affiliation CRUD (relate / unrelate / kick_vp / abort_vp).
|
|
1833
|
+
*
|
|
1834
|
+
* Single envelope so the UI doesn't fan out four separate WS message types
|
|
1835
|
+
* for housekeeping verbs. Replies with `unify_task_crud_result`.
|
|
1836
|
+
*
|
|
1837
|
+
* - relate { taskId, relatedTaskId } — bidirectional Δ27 link
|
|
1838
|
+
* - unrelate { taskId, relatedTaskId } — drop both directions
|
|
1839
|
+
* - kick_vp { taskId, vpId } — taskStore.removeMember
|
|
1840
|
+
* - abort_vp { taskId, vpId } — abort that VP's in-flight engine inside the task
|
|
1841
|
+
*/
|
|
1842
|
+
export async function handleUnifyTaskCrud(msg = {}) {
|
|
1843
|
+
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
1844
|
+
const op = typeof msg.op === 'string' ? msg.op : null;
|
|
1845
|
+
const taskId = typeof msg.taskId === 'string' ? msg.taskId : null;
|
|
1846
|
+
const vpId = typeof msg.vpId === 'string' ? msg.vpId : null;
|
|
1847
|
+
const relatedTaskId = typeof msg.relatedTaskId === 'string' ? msg.relatedTaskId : null;
|
|
1848
|
+
|
|
1849
|
+
const reply = (extra = {}) => sendUnifyEvent({
|
|
1850
|
+
type: 'unify_task_crud_result',
|
|
1851
|
+
op,
|
|
1852
|
+
taskId,
|
|
1853
|
+
...(vpId ? { vpId } : {}),
|
|
1854
|
+
...extra,
|
|
1855
|
+
...(requestId ? { requestId } : {}),
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
if (!op) { reply({ ok: false, error: 'missing_op' }); return; }
|
|
1859
|
+
if (!taskId) { reply({ ok: false, error: 'missing_task_id' }); return; }
|
|
1860
|
+
|
|
1861
|
+
try {
|
|
1862
|
+
const { getTaskStore } = await import('./tools/task-tools.js');
|
|
1863
|
+
const taskStore = getTaskStore();
|
|
1864
|
+
const task = taskStore?.get(taskId);
|
|
1865
|
+
if (!task) { reply({ ok: false, error: 'task_not_found' }); return; }
|
|
1866
|
+
|
|
1867
|
+
if (op === 'relate' || op === 'unrelate') {
|
|
1868
|
+
if (!relatedTaskId) { reply({ ok: false, error: 'missing_related_task_id' }); return; }
|
|
1869
|
+
const other = taskStore.get(relatedTaskId);
|
|
1870
|
+
if (!other) { reply({ ok: false, error: 'related_task_not_found' }); return; }
|
|
1871
|
+
const apply = (t, otherId, add) => {
|
|
1872
|
+
const cur = Array.isArray(t.relatedTaskIds) ? t.relatedTaskIds.slice() : [];
|
|
1873
|
+
const idx = cur.indexOf(otherId);
|
|
1874
|
+
if (add && idx === -1) cur.push(otherId);
|
|
1875
|
+
if (!add && idx !== -1) cur.splice(idx, 1);
|
|
1876
|
+
taskStore.update(t.id, { relatedTaskIds: cur });
|
|
1877
|
+
};
|
|
1878
|
+
apply(task, relatedTaskId, op === 'relate');
|
|
1879
|
+
apply(other, taskId, op === 'relate');
|
|
1880
|
+
reply({ ok: true, relatedTaskId });
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (op === 'kick_vp') {
|
|
1885
|
+
if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
|
|
1886
|
+
taskStore.removeMember(taskId, vpId);
|
|
1887
|
+
reply({ ok: true });
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (op === 'abort_vp') {
|
|
1892
|
+
if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
|
|
1893
|
+
// Reuse per-thread abort registry — keyed by (taskId,vpId) tuple if
|
|
1894
|
+
// the engine instance is registered there. For v1 we surface success
|
|
1895
|
+
// and let the engine settle; full per-VP cancellation is owned by
|
|
1896
|
+
// the engine registry in 334o follow-up.
|
|
1897
|
+
const reg = session?.engineRegistry;
|
|
1898
|
+
if (reg && typeof reg.abortVpInTask === 'function') {
|
|
1899
|
+
reg.abortVpInTask(taskId, vpId);
|
|
1900
|
+
}
|
|
1901
|
+
reply({ ok: true });
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
reply({ ok: false, error: 'unknown_op' });
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
reply({ ok: false, error: String(err?.message || err) });
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1638
1911
|
/**
|
|
1639
1912
|
* task-313: merge a source thread into a target thread.
|
|
1640
1913
|
* Reassigns messages, archives source with `mergedInto`, terminates source
|