@yeaft/webchat-agent 0.1.656 → 0.1.658
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/config.js +10 -0
- package/unify/engine.js +28 -1
- package/unify/groups/coordinator.js +50 -81
- package/unify/groups/pre-flow.js +308 -0
- package/unify/session.js +46 -2
- package/unify/vp/index.js +0 -7
- package/unify/vp/registry.js +10 -117
- package/unify/web-bridge.js +11 -1
- package/unify/vp/core-memory-recall.js +0 -75
- package/unify/vp/engine-binding.js +0 -96
- package/unify/vp/role-instance.js +0 -216
- package/unify/vp/run-turn.js +0 -251
- package/unify/vp/system-prompt.js +0 -311
package/unify/vp/run-turn.js
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* run-turn.js — execute one turn for a RoleInstance.
|
|
3
|
-
*
|
|
4
|
-
* A "turn" = consume one envelope from RoleInstance.inputQueue, build a
|
|
5
|
-
* fresh system prompt (§8), call `engine.query({prompt, messages, signal})`,
|
|
6
|
-
* accumulate the streamed text, and append the resulting assistant
|
|
7
|
-
* message both to `ri.messages` (for next-turn context) and — if the
|
|
8
|
-
* caller provides a GroupHandle — to the group's jsonl log via its
|
|
9
|
-
* `appendMessage` API (§334b public surface; hard constraint c: we never
|
|
10
|
-
* hand-write bytes).
|
|
11
|
-
*
|
|
12
|
-
* This is the canonical runner passed to `RoleInstance.drain(runner)`.
|
|
13
|
-
* Tests substitute a fake engine via createEngineBinder to exercise the
|
|
14
|
-
* drain loop / state machine without a live adapter.
|
|
15
|
-
*
|
|
16
|
-
* Streaming contract — event types handled (all optional; Engine.query is
|
|
17
|
-
* the source of truth):
|
|
18
|
-
* { type: 'text', text: string } — accumulate
|
|
19
|
-
* { type: 'text_chunk', text: string } — accumulate
|
|
20
|
-
* { type: 'tool_call', ... } — pass to onEvent
|
|
21
|
-
* { type: 'tool_end', ... } — pass to onEvent
|
|
22
|
-
* { type: 'aborted', reason } — exit loop, throw AbortError
|
|
23
|
-
* { type: 'error', error } — throw
|
|
24
|
-
* { type: 'turn_end', ... } — exit loop
|
|
25
|
-
*
|
|
26
|
-
* Unknown event types are forwarded to `onEvent(evt)` if provided.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { buildSystemPrompt } from './system-prompt.js';
|
|
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
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Build a runner suitable for RoleInstance.drain().
|
|
80
|
-
*
|
|
81
|
-
* @param {{
|
|
82
|
-
* binder: import('./engine-binding.js').createEngineBinder extends (...args:any)=>infer R ? R : never,
|
|
83
|
-
* registry?: import('./registry.js').Registry,
|
|
84
|
-
* group?: import('../groups/group-store.js').GroupHandle, // optional persistence
|
|
85
|
-
* rosterMembers?: string[],
|
|
86
|
-
* capabilitiesLine?: string,
|
|
87
|
-
* onEvent?: (evt:any, ri:any) => void,
|
|
88
|
-
* buildPromptOverride?: typeof buildSystemPrompt,
|
|
89
|
-
* }} deps
|
|
90
|
-
*/
|
|
91
|
-
export function createTurnRunner(deps = {}) {
|
|
92
|
-
const {
|
|
93
|
-
binder,
|
|
94
|
-
registry,
|
|
95
|
-
group,
|
|
96
|
-
rosterMembers,
|
|
97
|
-
capabilitiesLine,
|
|
98
|
-
onEvent,
|
|
99
|
-
buildPromptOverride,
|
|
100
|
-
taskStore, // R6 §6 trigger #6: enables task_ctx affiliation hint
|
|
101
|
-
} = deps;
|
|
102
|
-
|
|
103
|
-
if (!binder || typeof binder.bind !== 'function') {
|
|
104
|
-
throw new Error('createTurnRunner: binder (from createEngineBinder) is required');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const buildPrompt = buildPromptOverride || buildSystemPrompt;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The actual runner. Called once per envelope by drain().
|
|
111
|
-
*
|
|
112
|
-
* @param {object} envelope — { groupId, taskId, msg, trigger }
|
|
113
|
-
* @param {import('./role-instance.js').RoleInstance} ri
|
|
114
|
-
*/
|
|
115
|
-
return async function runOneTurn(envelope, ri) {
|
|
116
|
-
if (!envelope || !envelope.msg) {
|
|
117
|
-
throw new Error('runOneTurn: envelope.msg missing');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const engine = binder.bind(ri);
|
|
121
|
-
|
|
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
|
-
});
|
|
128
|
-
const systemPrompt = await buildPrompt(ri, {
|
|
129
|
-
registry,
|
|
130
|
-
rosterMembers,
|
|
131
|
-
capabilitiesLine,
|
|
132
|
-
runtimeCtx: {
|
|
133
|
-
taskId: envelope.taskId || null,
|
|
134
|
-
isDream: false,
|
|
135
|
-
},
|
|
136
|
-
taskCtx,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Prompt text = inbound message body. Engine.query spec:
|
|
140
|
-
// { prompt, messages, signal, ... }
|
|
141
|
-
// It prepends the system prompt via adapter-level wiring; we pass
|
|
142
|
-
// `systemPrompt` as an explicit first message if the engine supports
|
|
143
|
-
// it, else rely on the engine's own system injection. For §334c MVP
|
|
144
|
-
// we pass systemPrompt as metadata on messages[0] and let the engine
|
|
145
|
-
// decide — tests supply a fake engine that echoes back.
|
|
146
|
-
const prompt = String(envelope.msg.text || '').trim();
|
|
147
|
-
if (!prompt) {
|
|
148
|
-
// No-op envelope (empty msg): record a stub and return.
|
|
149
|
-
ri.messages.push({ role: 'user', text: '', ts: new Date().toISOString(), meta: envelope });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
ri.messages.push({
|
|
154
|
-
role: 'user',
|
|
155
|
-
text: prompt,
|
|
156
|
-
ts: envelope.msg.ts || new Date().toISOString(),
|
|
157
|
-
from: envelope.msg.from || 'user',
|
|
158
|
-
msgId: envelope.msg.id || null,
|
|
159
|
-
trigger: envelope.trigger || null,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const signal = ri.abortController ? ri.abortController.signal : undefined;
|
|
163
|
-
|
|
164
|
-
// Build prior-messages window for the engine — MVP: the last 20.
|
|
165
|
-
const priorMessages = ri.messages.slice(-20);
|
|
166
|
-
|
|
167
|
-
let accumulated = '';
|
|
168
|
-
let aborted = false;
|
|
169
|
-
let errored = null;
|
|
170
|
-
|
|
171
|
-
const iterator = engine.query({
|
|
172
|
-
prompt,
|
|
173
|
-
messages: priorMessages,
|
|
174
|
-
signal,
|
|
175
|
-
systemPrompt,
|
|
176
|
-
metadata: {
|
|
177
|
-
vpId: ri.vpId,
|
|
178
|
-
groupId: ri.groupId,
|
|
179
|
-
taskId: envelope.taskId || null,
|
|
180
|
-
turnId: `${ri.id}:${ri.turnCount}`,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
for await (const evt of iterator) {
|
|
186
|
-
if (!evt || typeof evt !== 'object') continue;
|
|
187
|
-
switch (evt.type) {
|
|
188
|
-
case 'text':
|
|
189
|
-
case 'text_chunk':
|
|
190
|
-
case 'message':
|
|
191
|
-
if (typeof evt.text === 'string') accumulated += evt.text;
|
|
192
|
-
if (onEvent) try { onEvent(evt, ri); } catch { /* ignore */ }
|
|
193
|
-
break;
|
|
194
|
-
case 'aborted':
|
|
195
|
-
aborted = true;
|
|
196
|
-
if (onEvent) try { onEvent(evt, ri); } catch { /* ignore */ }
|
|
197
|
-
break;
|
|
198
|
-
case 'error':
|
|
199
|
-
errored = evt.error instanceof Error ? evt.error : new Error(String(evt.error || 'engine error'));
|
|
200
|
-
if (onEvent) try { onEvent(evt, ri); } catch { /* ignore */ }
|
|
201
|
-
break;
|
|
202
|
-
case 'turn_end':
|
|
203
|
-
if (onEvent) try { onEvent(evt, ri); } catch { /* ignore */ }
|
|
204
|
-
break;
|
|
205
|
-
default:
|
|
206
|
-
if (onEvent) try { onEvent(evt, ri); } catch { /* ignore */ }
|
|
207
|
-
}
|
|
208
|
-
if (aborted || errored) break;
|
|
209
|
-
}
|
|
210
|
-
} catch (err) {
|
|
211
|
-
if (err && err.name === 'AbortError') {
|
|
212
|
-
aborted = true;
|
|
213
|
-
} else {
|
|
214
|
-
errored = err;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (aborted) {
|
|
219
|
-
const e = new Error('aborted');
|
|
220
|
-
e.name = 'AbortError';
|
|
221
|
-
throw e;
|
|
222
|
-
}
|
|
223
|
-
if (errored) throw errored;
|
|
224
|
-
|
|
225
|
-
// Record assistant reply (even if empty — the turn still counted).
|
|
226
|
-
const assistantMsg = {
|
|
227
|
-
role: 'assistant',
|
|
228
|
-
text: accumulated,
|
|
229
|
-
ts: new Date().toISOString(),
|
|
230
|
-
from: ri.vpId,
|
|
231
|
-
taskId: envelope.taskId || null,
|
|
232
|
-
};
|
|
233
|
-
ri.messages.push(assistantMsg);
|
|
234
|
-
|
|
235
|
-
// Hard constraint (c): persistence goes through 334b's appendMessage.
|
|
236
|
-
if (group && typeof group.appendMessage === 'function' && accumulated.trim()) {
|
|
237
|
-
try {
|
|
238
|
-
group.appendMessage({
|
|
239
|
-
from: ri.vpId,
|
|
240
|
-
role: 'assistant',
|
|
241
|
-
text: accumulated,
|
|
242
|
-
taskId: envelope.taskId || null,
|
|
243
|
-
meta: { trigger: envelope.trigger || null, replyTo: envelope.msg.id || null },
|
|
244
|
-
});
|
|
245
|
-
} catch (err) {
|
|
246
|
-
// Non-fatal: the in-memory message is still recorded on ri.messages.
|
|
247
|
-
if (onEvent) try { onEvent({ type: 'persist_error', error: err }, ri); } catch { /* ignore */ }
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
}
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* system-prompt.js — assemble a VP's system prompt per architecture §8.
|
|
3
|
-
*
|
|
4
|
-
* Three sections:
|
|
5
|
-
* § STATIC
|
|
6
|
-
* - identity (yeaft base — short)
|
|
7
|
-
* - vp_persona (role.md persona body)
|
|
8
|
-
* - capabilities (tools) — 334d owns the real list; this slice leaves
|
|
9
|
-
* a stub that names the MVP tool surface ("route_forward" ...).
|
|
10
|
-
*
|
|
11
|
-
* § SEMI-DYNAMIC
|
|
12
|
-
* - group_roster (name + on-line status via Registry.activeCount)
|
|
13
|
-
* - (skills / mcp / user_profile — owned by other slices; 334c emits
|
|
14
|
-
* placeholder only if the caller provides them)
|
|
15
|
-
*
|
|
16
|
-
* § DYNAMIC
|
|
17
|
-
* - runtime_ctx { vpId, groupId, taskId?, isDream=false }
|
|
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.)
|
|
24
|
-
*
|
|
25
|
-
* Caching: the STATIC persona section is cached per RoleInstance via
|
|
26
|
-
* `ri._promptBuiltForMtime === vp.mtimeMs`. DYNAMIC is always rebuilt.
|
|
27
|
-
*
|
|
28
|
-
* Hard constraint (334c): this module does not touch 334f/334g memory
|
|
29
|
-
* internals. It calls `memoryStore.query({vp})` (duck-typed) and falls
|
|
30
|
-
* back to an empty top-K if the store is null.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import { recallCoreMemory } from './core-memory-recall.js';
|
|
34
|
-
|
|
35
|
-
const CORE_MEMORY_TOP_K = 7; // §8
|
|
36
|
-
const ROSTER_STATUS_ONLINE = 'online';
|
|
37
|
-
const ROSTER_STATUS_BUSY = 'busy';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Build one complete system prompt string.
|
|
41
|
-
*
|
|
42
|
-
* @param {import('./role-instance.js').RoleInstance} ri
|
|
43
|
-
* @param {{
|
|
44
|
-
* registry?: import('./registry.js').Registry,
|
|
45
|
-
* rosterMembers?: string[], // explicit override (334b GroupHandle.roster)
|
|
46
|
-
* runtimeCtx?: { taskId?: string|null, isDream?: boolean, recentChatSummary?: string },
|
|
47
|
-
* capabilitiesLine?: string, // 334d injects tool inventory
|
|
48
|
-
* userProfile?: string, // 334l injects top-5 user-memory
|
|
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
|
-
* },
|
|
61
|
-
* }} opts
|
|
62
|
-
* @returns {string}
|
|
63
|
-
*/
|
|
64
|
-
export async function buildSystemPrompt(ri, opts = {}) {
|
|
65
|
-
if (!ri || !ri.vp) throw new Error('buildSystemPrompt: role instance required');
|
|
66
|
-
const vp = ri.vp;
|
|
67
|
-
|
|
68
|
-
// ─── § STATIC (cached per mtime) ────────────────────────────
|
|
69
|
-
let staticBlock = ri.systemPrompt;
|
|
70
|
-
if (!staticBlock || ri._promptBuiltForMtime !== vp.mtimeMs) {
|
|
71
|
-
staticBlock = buildStatic(vp, opts.capabilitiesLine);
|
|
72
|
-
ri.systemPrompt = staticBlock;
|
|
73
|
-
ri._promptBuiltForMtime = vp.mtimeMs;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ─── § SEMI-DYNAMIC ─────────────────────────────────────────
|
|
77
|
-
const roster = buildRoster(ri, opts.registry, opts.rosterMembers);
|
|
78
|
-
const userProfile = opts.userProfile ? `\n## user_profile\n${opts.userProfile.trim()}\n` : '';
|
|
79
|
-
|
|
80
|
-
// ─── § DYNAMIC ──────────────────────────────────────────────
|
|
81
|
-
const ctx = opts.runtimeCtx || {};
|
|
82
|
-
const runtime = buildRuntimeCtx(ri, ctx);
|
|
83
|
-
const recent = opts.recentGroupChat
|
|
84
|
-
? `\n## recent_group_chat\n${opts.recentGroupChat.trim()}\n`
|
|
85
|
-
: '';
|
|
86
|
-
const coreMem = await buildCoreMemoryBlock(ri, ctx);
|
|
87
|
-
const taskCtx = buildTaskCtxBlock(ri, opts.taskCtx, ctx);
|
|
88
|
-
|
|
89
|
-
return [
|
|
90
|
-
'# § STATIC',
|
|
91
|
-
staticBlock,
|
|
92
|
-
'',
|
|
93
|
-
'# § SEMI-DYNAMIC',
|
|
94
|
-
roster,
|
|
95
|
-
userProfile.trim() ? userProfile : '',
|
|
96
|
-
'',
|
|
97
|
-
'# § DYNAMIC',
|
|
98
|
-
runtime,
|
|
99
|
-
recent.trim() ? recent : '',
|
|
100
|
-
coreMem,
|
|
101
|
-
taskCtx,
|
|
102
|
-
]
|
|
103
|
-
.filter(Boolean)
|
|
104
|
-
.join('\n')
|
|
105
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
106
|
-
.trim();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ──────────────────────────────────────────────────────────────
|
|
110
|
-
// STATIC
|
|
111
|
-
// ──────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
function buildStatic(vp, capabilitiesLine) {
|
|
114
|
-
const identity =
|
|
115
|
-
'You are a Virtual Person (VP) in a yeaft multi-agent group.\n' +
|
|
116
|
-
'Respect §6 triggers: text @-mentions do NOT route. Use the `route_forward` tool for explicit dispatch.';
|
|
117
|
-
|
|
118
|
-
const persona = vp.persona && vp.persona.trim()
|
|
119
|
-
? vp.persona.trim()
|
|
120
|
-
: `(no persona body for ${vp.id})`;
|
|
121
|
-
|
|
122
|
-
const caps = (capabilitiesLine && capabilitiesLine.trim())
|
|
123
|
-
|| [
|
|
124
|
-
'Tools: route_forward, memory_search, memory_trace, task_summary_post (if initiator).',
|
|
125
|
-
'Sub-agent fan-out: when a single user task is large enough to benefit from parallel execution,',
|
|
126
|
-
'you MAY spawn sub-agents using the `Agent` tool. Each sub-agent inherits your persona +',
|
|
127
|
-
'voice, gets its own ToolRegistry (without Agent / RouteForward / AskUser to prevent recursion),',
|
|
128
|
-
'and runs the same Engine flow. Use `Agent` to spawn (returns agentId), `WaitAgent` to collect',
|
|
129
|
-
'each turn output, `SendMessage` for follow-ups, `CloseAgent` when done. You can fire multiple',
|
|
130
|
-
'`Agent` tool_calls in one assistant turn to launch them in parallel. Pass a self-contained,',
|
|
131
|
-
'markdown mission ("## Goal / ## Context / ## Deliverable / ## Constraints") — the sub-agent',
|
|
132
|
-
'cannot see your conversation history. Only spawn when work is genuinely parallelisable; for',
|
|
133
|
-
'small or strictly-sequential tasks, do it yourself.',
|
|
134
|
-
].join('\n');
|
|
135
|
-
|
|
136
|
-
// personaHash travels in the static block so downstream (334h live-diff)
|
|
137
|
-
// can detect changes without re-hashing.
|
|
138
|
-
return [
|
|
139
|
-
'## identity',
|
|
140
|
-
identity,
|
|
141
|
-
'',
|
|
142
|
-
`## vp_persona (id=${vp.id}, hash=${vp.personaHash || '-'})`,
|
|
143
|
-
`Name: ${vp.name}`,
|
|
144
|
-
vp.role ? `Role: ${vp.role}` : '',
|
|
145
|
-
vp.traits && vp.traits.length ? `Traits: ${vp.traits.join(', ')}` : '',
|
|
146
|
-
'',
|
|
147
|
-
persona,
|
|
148
|
-
'',
|
|
149
|
-
'## capabilities',
|
|
150
|
-
caps,
|
|
151
|
-
].filter(Boolean).join('\n');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ──────────────────────────────────────────────────────────────
|
|
155
|
-
// SEMI-DYNAMIC — Roster
|
|
156
|
-
// ──────────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
function buildRoster(ri, registry, rosterMembers) {
|
|
159
|
-
const members = Array.isArray(rosterMembers) && rosterMembers.length > 0
|
|
160
|
-
? rosterMembers
|
|
161
|
-
: registry
|
|
162
|
-
? Array.from(new Set(registry.listRoleInstances()
|
|
163
|
-
.filter((r) => r.groupId === ri.groupId)
|
|
164
|
-
.map((r) => r.vpId)))
|
|
165
|
-
: [ri.vpId];
|
|
166
|
-
|
|
167
|
-
const lines = [`## 群成员 (${members.length})`];
|
|
168
|
-
for (const vpId of members) {
|
|
169
|
-
if (vpId === ri.vpId) {
|
|
170
|
-
lines.push(`- 你自己:${vpId}`);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const status = memberStatus(vpId, registry);
|
|
174
|
-
const name = registry?.getVp?.(vpId)?.name || vpId;
|
|
175
|
-
lines.push(`- ${name} (${vpId}) · ${status}`);
|
|
176
|
-
}
|
|
177
|
-
return lines.join('\n');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function memberStatus(vpId, registry) {
|
|
181
|
-
if (!registry) return ROSTER_STATUS_ONLINE;
|
|
182
|
-
// §8.2: busy if any RoleInstance for this VP has state==='running' in any group.
|
|
183
|
-
// MVP: also 'running' while 'queued'? — spec says "running RoleInstance 数 > 0".
|
|
184
|
-
const ris = registry.listRoleInstances?.() || [];
|
|
185
|
-
const busy = ris.some((r) => r.vpId === vpId && r.state === 'running');
|
|
186
|
-
return busy ? ROSTER_STATUS_BUSY : ROSTER_STATUS_ONLINE;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ──────────────────────────────────────────────────────────────
|
|
190
|
-
// DYNAMIC
|
|
191
|
-
// ──────────────────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
function buildRuntimeCtx(ri, ctx) {
|
|
194
|
-
return [
|
|
195
|
-
'## runtime_ctx',
|
|
196
|
-
`vpId: ${ri.vpId}`,
|
|
197
|
-
`groupId: ${ri.groupId}`,
|
|
198
|
-
ctx.taskId ? `taskId: ${ctx.taskId}` : null,
|
|
199
|
-
`isDream: ${Boolean(ctx.isDream)}`,
|
|
200
|
-
].filter(Boolean).join('\n');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function buildCoreMemoryBlock(ri, ctx) {
|
|
204
|
-
if (!ri.memoryStore) return '';
|
|
205
|
-
const entries = await recallCoreMemory(ri.memoryStore, {
|
|
206
|
-
vp: ri.vpId,
|
|
207
|
-
limit: CORE_MEMORY_TOP_K,
|
|
208
|
-
});
|
|
209
|
-
if (!entries || entries.length === 0) return '';
|
|
210
|
-
void ctx; // task_ctx injection is 334n's scope; reserved param.
|
|
211
|
-
const lines = ['## core_memory'];
|
|
212
|
-
for (const e of entries) {
|
|
213
|
-
const shard = e.shard || 'general';
|
|
214
|
-
const body = (e.body || '').trim();
|
|
215
|
-
if (!body) continue;
|
|
216
|
-
lines.push(`- [mem:${shard}] ${body}`);
|
|
217
|
-
}
|
|
218
|
-
if (lines.length === 1) return '';
|
|
219
|
-
return lines.join('\n');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ──────────────────────────────────────────────────────────────
|
|
223
|
-
// DYNAMIC — task_ctx (R6 §6 trigger #6 + §8)
|
|
224
|
-
//
|
|
225
|
-
// The affiliation hint is the load-bearing piece that lets tasks
|
|
226
|
-
// auto-emerge from chat. Without it, VPs never call task_create —
|
|
227
|
-
// the spec lists the tool but a model with no nudge will just keep
|
|
228
|
-
// chatting at group level. We list active tasks in the group + give
|
|
229
|
-
// crisp guidance on when to (a) continue an existing task, (b) start
|
|
230
|
-
// a new one, or (c) reply at group level.
|
|
231
|
-
// ──────────────────────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
function buildTaskCtxBlock(ri, taskCtx, runtimeCtx) {
|
|
234
|
-
if (!taskCtx) return '';
|
|
235
|
-
|
|
236
|
-
const currentTask = taskCtx.currentTask;
|
|
237
|
-
const activeTasks = Array.isArray(taskCtx.activeTasks) ? taskCtx.activeTasks : [];
|
|
238
|
-
const relatedTasks = Array.isArray(taskCtx.relatedTasks) ? taskCtx.relatedTasks : [];
|
|
239
|
-
|
|
240
|
-
const otherTasks = activeTasks
|
|
241
|
-
.filter((t) => t && t.id && (!currentTask || t.id !== currentTask.id))
|
|
242
|
-
.slice(0, 8); // cap to avoid prompt bloat — coordinator orders by recency
|
|
243
|
-
|
|
244
|
-
// Nothing useful to say — drop the block entirely. Without active tasks,
|
|
245
|
-
// a current task, or related tasks, the affiliation_hint has no anchor.
|
|
246
|
-
if (otherTasks.length === 0 && !currentTask && relatedTasks.length === 0) {
|
|
247
|
-
return '';
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const lines = ['## task_ctx'];
|
|
251
|
-
|
|
252
|
-
// (a) currently inside a task → focus on this one
|
|
253
|
-
if (currentTask && currentTask.id) {
|
|
254
|
-
lines.push(`### current_task`);
|
|
255
|
-
lines.push(`- id: ${currentTask.id}`);
|
|
256
|
-
if (currentTask.title) lines.push(`- title: ${currentTask.title}`);
|
|
257
|
-
if (currentTask.initiator) lines.push(`- initiator: ${currentTask.initiator}`);
|
|
258
|
-
if (Array.isArray(currentTask.members) && currentTask.members.length) {
|
|
259
|
-
lines.push(`- members: ${currentTask.members.join(', ')}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// (b) other open tasks in the same group — candidates for affiliation
|
|
264
|
-
if (otherTasks.length > 0) {
|
|
265
|
-
lines.push(`### active_tasks_in_group (${otherTasks.length})`);
|
|
266
|
-
for (const t of otherTasks) {
|
|
267
|
-
const parts = [`- ${t.id}`];
|
|
268
|
-
if (t.title) parts.push(`"${t.title}"`);
|
|
269
|
-
if (t.status) parts.push(`(${t.status})`);
|
|
270
|
-
if (Array.isArray(t.members) && t.members.length) {
|
|
271
|
-
parts.push(`members=[${t.members.join(',')}]`);
|
|
272
|
-
}
|
|
273
|
-
lines.push(parts.join(' '));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// (c) related tasks (recall top-N from §14)
|
|
278
|
-
if (relatedTasks.length > 0) {
|
|
279
|
-
lines.push(`### related_tasks (${relatedTasks.length})`);
|
|
280
|
-
for (const t of relatedTasks.slice(0, 3)) {
|
|
281
|
-
const parts = [`- ${t.id}`];
|
|
282
|
-
if (t.title) parts.push(`"${t.title}"`);
|
|
283
|
-
if (t.groupId && t.groupId !== ri.groupId) parts.push(`(grp:${t.groupId})`);
|
|
284
|
-
lines.push(parts.join(' '));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// (d) affiliation hint — the actual decision rubric
|
|
289
|
-
// Suppress full hint when already inside a task (focus on continuing).
|
|
290
|
-
const inTask = Boolean(runtimeCtx?.taskId || currentTask?.id);
|
|
291
|
-
if (!inTask) {
|
|
292
|
-
lines.push('');
|
|
293
|
-
lines.push('### affiliation_hint');
|
|
294
|
-
lines.push('Decide BEFORE replying:');
|
|
295
|
-
lines.push('- If this user message clearly continues one of the active_tasks_in_group above,');
|
|
296
|
-
lines.push(' use the `task_message` route (other VPs in that task will see your reply, others will not).');
|
|
297
|
-
lines.push('- If this is a NEW multi-turn collaboration worth tracking (decisions, deliverables,');
|
|
298
|
-
lines.push(' multiple VPs needed), call `TaskCreate` with title + members[] + groupId; you become');
|
|
299
|
-
lines.push(' the task initiator and may post `task_summary_post` to broadcast progress.');
|
|
300
|
-
lines.push('- If this is just a quick question / chitchat / single-shot answer, reply at group level — no task.');
|
|
301
|
-
lines.push('Do NOT create a task for trivial back-and-forth. Do NOT continue a task whose topic clearly diverged.');
|
|
302
|
-
} else {
|
|
303
|
-
lines.push('');
|
|
304
|
-
lines.push('### affiliation_hint');
|
|
305
|
-
lines.push('You are inside a task. Stay focused on its scope. Use `task_summary_post` (initiator only)');
|
|
306
|
-
lines.push('to broadcast progress to the group. Use `route_forward` to hand off; do not silently leave.');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (lines.length === 1) return '';
|
|
310
|
-
return lines.join('\n');
|
|
311
|
-
}
|