@yeaft/webchat-agent 0.1.530 → 0.1.531
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 +17 -1
- package/package.json +1 -1
- package/unify/task-message.js +144 -0
- package/unify/user-memory.js +104 -0
- package/unify/web-bridge.js +39 -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, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead } from '../unify/web-bridge.js';
|
|
39
|
+
import { handleUnifyChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -420,6 +420,22 @@ export async function handleMessage(msg) {
|
|
|
420
420
|
handleUnifyVpRead(msg);
|
|
421
421
|
break;
|
|
422
422
|
|
|
423
|
+
// task-334h (R6 §Δ28 / §Δ31.6): task-scoped direct message echo.
|
|
424
|
+
// Replaces the withdrawn R3 `unify_task_private_chat`. Agent validates,
|
|
425
|
+
// stamps msgId + ts, and broadcasts the `task_message` mirror back.
|
|
426
|
+
case 'unify_task_message':
|
|
427
|
+
handleUnifyTaskMessage(msg);
|
|
428
|
+
break;
|
|
429
|
+
|
|
430
|
+
// task-334h (R6 §Δ29): user-memory skeleton. Payload schema + event
|
|
431
|
+
// names are wire-frozen here; real ingestion lands in task-334l.
|
|
432
|
+
case 'unify_user_memory_write':
|
|
433
|
+
handleUnifyUserMemoryWrite(msg);
|
|
434
|
+
break;
|
|
435
|
+
case 'unify_user_memory_remove':
|
|
436
|
+
handleUnifyUserMemoryRemove(msg);
|
|
437
|
+
break;
|
|
438
|
+
|
|
423
439
|
// Expert roles definition (for ExpertPanel detail view)
|
|
424
440
|
case 'get_expert_roles': {
|
|
425
441
|
const { getExpertRolesDefinition } = await import('../expert-roles.js');
|
package/package.json
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-message.js — R6 §Δ28 / §Δ31.6 task-scoped direct messaging.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the withdrawn R3 `unify_task_private_chat` event with a simple
|
|
5
|
+
* echo-able `task_message` pair:
|
|
6
|
+
*
|
|
7
|
+
* inbound (web → agent): `unify_task_message`
|
|
8
|
+
* { type, groupId, taskId, vpId, text, mentions?, replyTo?, requestId? }
|
|
9
|
+
* outbound (agent → web): `task_message`
|
|
10
|
+
* { type, groupId, taskId, vpId, msgId, text, mentions, replyTo,
|
|
11
|
+
* ts, requestId? }
|
|
12
|
+
*
|
|
13
|
+
* This module owns only the *wire adapter* — the payload is validated,
|
|
14
|
+
* stamped with msgId + ts, and broadcast back so the sender's UI and any
|
|
15
|
+
* other connected views converge on the same record. Persistence + task
|
|
16
|
+
* ACL enforcement are deliberately deferred to task-334l (per PM dispatch:
|
|
17
|
+
* "user_memory_* 实际 ingestion 归 334l"); the parallel task-private
|
|
18
|
+
* storage hook follows the same phasing.
|
|
19
|
+
*
|
|
20
|
+
* Invariants:
|
|
21
|
+
* • Never throws on the WS hot path — bad payloads reply with a
|
|
22
|
+
* `task_message_rejected` event carrying a stable `code` string for
|
|
23
|
+
* UI i18n (mirrors the vp_crud_result contract from 334-ui-g).
|
|
24
|
+
* • The outbound event field order and keys are considered wire-frozen
|
|
25
|
+
* per R6 §Δ31.6 table; additive fields only in future slices.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { nextMsgId, isValidVpId } from './groups/ids.js';
|
|
29
|
+
|
|
30
|
+
/** Known `reject` codes — kept stable so 334-ui-* can key i18n on them. */
|
|
31
|
+
export const TASK_MESSAGE_REJECT_CODES = Object.freeze({
|
|
32
|
+
MISSING_GROUP_ID: 'missing_group_id',
|
|
33
|
+
MISSING_TASK_ID: 'missing_task_id',
|
|
34
|
+
MISSING_VP_ID: 'missing_vp_id',
|
|
35
|
+
INVALID_VP_ID: 'invalid_vp_id',
|
|
36
|
+
EMPTY_TEXT: 'empty_text',
|
|
37
|
+
TEXT_TOO_LONG: 'text_too_long',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Soft body cap — matches the shard-entry cap used elsewhere in R6 (§Δ26.3). */
|
|
41
|
+
export const MAX_TEXT_LENGTH = 16_384;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pure validator. Returns `{ ok: true, payload }` or `{ ok: false, code }`.
|
|
45
|
+
* No IO, no clock reads — safe to unit-test in isolation.
|
|
46
|
+
*
|
|
47
|
+
* @param {any} msg — raw WS message from the web client
|
|
48
|
+
*/
|
|
49
|
+
export function validateTaskMessage(msg) {
|
|
50
|
+
if (!msg || typeof msg !== 'object') {
|
|
51
|
+
return { ok: false, code: TASK_MESSAGE_REJECT_CODES.MISSING_GROUP_ID };
|
|
52
|
+
}
|
|
53
|
+
const { groupId, taskId, vpId, text } = msg;
|
|
54
|
+
if (!groupId || typeof groupId !== 'string') {
|
|
55
|
+
return { ok: false, code: TASK_MESSAGE_REJECT_CODES.MISSING_GROUP_ID };
|
|
56
|
+
}
|
|
57
|
+
if (!taskId || typeof taskId !== 'string') {
|
|
58
|
+
return { ok: false, code: TASK_MESSAGE_REJECT_CODES.MISSING_TASK_ID };
|
|
59
|
+
}
|
|
60
|
+
if (!vpId || typeof vpId !== 'string') {
|
|
61
|
+
return { ok: false, code: TASK_MESSAGE_REJECT_CODES.MISSING_VP_ID };
|
|
62
|
+
}
|
|
63
|
+
// Allow the reserved `user` sentinel as a speaker here — tasks can have
|
|
64
|
+
// human-user messages alongside VP messages. Any other vpId must pass
|
|
65
|
+
// the full shape check (rejects `all`, `system`, pure digits, etc.).
|
|
66
|
+
if (vpId !== 'user' && !isValidVpId(vpId)) {
|
|
67
|
+
return { ok: false, code: TASK_MESSAGE_REJECT_CODES.INVALID_VP_ID };
|
|
68
|
+
}
|
|
69
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
70
|
+
return { ok: false, code: TASK_MESSAGE_REJECT_CODES.EMPTY_TEXT };
|
|
71
|
+
}
|
|
72
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
73
|
+
return { ok: false, code: TASK_MESSAGE_REJECT_CODES.TEXT_TOO_LONG };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const mentions = Array.isArray(msg.mentions)
|
|
77
|
+
? msg.mentions.filter(m => typeof m === 'string' && m.length > 0).slice(0, 32)
|
|
78
|
+
: [];
|
|
79
|
+
const replyTo = typeof msg.replyTo === 'string' && msg.replyTo.length > 0
|
|
80
|
+
? msg.replyTo
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
payload: { groupId, taskId, vpId, text, mentions, replyTo },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the outbound `task_message` event from a validated payload.
|
|
91
|
+
* Exposed separately so tests can snapshot the wire shape without
|
|
92
|
+
* needing a live send fn.
|
|
93
|
+
*
|
|
94
|
+
* @param {{groupId:string,taskId:string,vpId:string,text:string,mentions:string[],replyTo:?string}} payload
|
|
95
|
+
* @param {{now?:()=>number, msgId?:()=>string, requestId?:string}} [opts]
|
|
96
|
+
*/
|
|
97
|
+
export function buildTaskMessageEvent(payload, opts = {}) {
|
|
98
|
+
const now = typeof opts.now === 'function' ? opts.now : Date.now;
|
|
99
|
+
const mkId = typeof opts.msgId === 'function' ? opts.msgId : nextMsgId;
|
|
100
|
+
const evt = {
|
|
101
|
+
type: 'task_message',
|
|
102
|
+
groupId: payload.groupId,
|
|
103
|
+
taskId: payload.taskId,
|
|
104
|
+
vpId: payload.vpId,
|
|
105
|
+
msgId: mkId(),
|
|
106
|
+
text: payload.text,
|
|
107
|
+
mentions: payload.mentions,
|
|
108
|
+
replyTo: payload.replyTo,
|
|
109
|
+
ts: now(),
|
|
110
|
+
};
|
|
111
|
+
if (opts.requestId) evt.requestId = opts.requestId;
|
|
112
|
+
return evt;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the outbound `task_message_rejected` event.
|
|
117
|
+
* @param {string} code — one of TASK_MESSAGE_REJECT_CODES
|
|
118
|
+
* @param {any} msg — original inbound msg (for requestId echo)
|
|
119
|
+
*/
|
|
120
|
+
export function buildTaskMessageRejected(code, msg) {
|
|
121
|
+
const evt = { type: 'task_message_rejected', code };
|
|
122
|
+
if (msg && typeof msg.requestId === 'string') evt.requestId = msg.requestId;
|
|
123
|
+
if (msg && typeof msg.groupId === 'string') evt.groupId = msg.groupId;
|
|
124
|
+
if (msg && typeof msg.taskId === 'string') evt.taskId = msg.taskId;
|
|
125
|
+
return evt;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* WS handler entry point. Validates, echoes, never throws.
|
|
130
|
+
*
|
|
131
|
+
* @param {any} msg
|
|
132
|
+
* @param {(event:object)=>void} sendUnifyEvent
|
|
133
|
+
* @param {{now?:()=>number, msgId?:()=>string}} [opts] — test seams
|
|
134
|
+
*/
|
|
135
|
+
export function handleUnifyTaskMessage(msg, sendUnifyEvent, opts = {}) {
|
|
136
|
+
const result = validateTaskMessage(msg);
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
try { sendUnifyEvent(buildTaskMessageRejected(result.code, msg)); } catch { /* best-effort */ }
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const requestId = msg && typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
142
|
+
const evt = buildTaskMessageEvent(result.payload, { ...opts, requestId });
|
|
143
|
+
try { sendUnifyEvent(evt); } catch { /* never crash WS pipeline */ }
|
|
144
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* user-memory.js — R6 §Δ29 user-memory WS event skeleton.
|
|
3
|
+
*
|
|
4
|
+
* PLACEHOLDER ONLY. Actual ingestion / shard write / cross-task recall
|
|
5
|
+
* is owned by task-334l. This file reserves three event names on the
|
|
6
|
+
* wire + acknowledges the request so the web client can ship its
|
|
7
|
+
* emitter code without a dependency-cycle on 334l's storage layer.
|
|
8
|
+
*
|
|
9
|
+
* Wire shapes (frozen by R6 §Δ31.6 table; additive fields only):
|
|
10
|
+
*
|
|
11
|
+
* inbound (web → agent): `unify_user_memory_write`
|
|
12
|
+
* { type, text, tags?, sourceRef?, requestId? }
|
|
13
|
+
*
|
|
14
|
+
* outbound (agent → web): `user_memory_updated`
|
|
15
|
+
* { type, entryId?, reason: 'accepted'|'deferred'|'noop',
|
|
16
|
+
* requestId?, pending?: boolean }
|
|
17
|
+
*
|
|
18
|
+
* outbound (agent → web): `user_memory_removed`
|
|
19
|
+
* { type, entryId, requestId? }
|
|
20
|
+
*
|
|
21
|
+
* Current behaviour: every write is replied with `user_memory_updated`
|
|
22
|
+
* carrying `reason: 'deferred'` and `pending: true` — the frontend
|
|
23
|
+
* treats this as "queued but not yet persisted" and keeps the toast in
|
|
24
|
+
* a muted state. 334l will flip the reason to `'accepted'` with a
|
|
25
|
+
* concrete `entryId` once the ingestion pipeline lands.
|
|
26
|
+
*
|
|
27
|
+
* No removal path is offered yet (would require the storage layer to
|
|
28
|
+
* have produced entryIds first); the handler is exported as a named
|
|
29
|
+
* stub so the router can wire it without a second edit when 334l ships.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** @type {(event:object)=>void | null} */
|
|
33
|
+
let _sendUnifyEvent = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Install a send fn. Called once during session init from web-bridge.js.
|
|
37
|
+
* Exposed so tests can swap in a collector without spinning up a session.
|
|
38
|
+
*/
|
|
39
|
+
export function setUserMemorySender(fn) {
|
|
40
|
+
_sendUnifyEvent = (typeof fn === 'function') ? fn : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* WS handler: `unify_user_memory_write`.
|
|
45
|
+
*
|
|
46
|
+
* Validates the minimum shape (non-empty string `text`) and replies with
|
|
47
|
+
* a `user_memory_updated` ack carrying `pending: true`. Never throws.
|
|
48
|
+
*
|
|
49
|
+
* @param {any} msg
|
|
50
|
+
* @param {(event:object)=>void} [sendUnifyEvent] — optional override
|
|
51
|
+
* (falls back to the module-level sender installed via setUserMemorySender)
|
|
52
|
+
*/
|
|
53
|
+
export function handleUnifyUserMemoryWrite(msg, sendUnifyEvent) {
|
|
54
|
+
const send = sendUnifyEvent || _sendUnifyEvent;
|
|
55
|
+
if (!send) return;
|
|
56
|
+
|
|
57
|
+
const requestId = msg && typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
58
|
+
const text = msg && typeof msg.text === 'string' ? msg.text : '';
|
|
59
|
+
|
|
60
|
+
if (!text || text.length === 0) {
|
|
61
|
+
try {
|
|
62
|
+
send({
|
|
63
|
+
type: 'user_memory_updated',
|
|
64
|
+
reason: 'noop',
|
|
65
|
+
pending: false,
|
|
66
|
+
...(requestId ? { requestId } : {}),
|
|
67
|
+
});
|
|
68
|
+
} catch { /* best-effort */ }
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Placeholder — 334l replaces this with real ingestion.
|
|
73
|
+
try {
|
|
74
|
+
send({
|
|
75
|
+
type: 'user_memory_updated',
|
|
76
|
+
reason: 'deferred',
|
|
77
|
+
pending: true,
|
|
78
|
+
...(requestId ? { requestId } : {}),
|
|
79
|
+
});
|
|
80
|
+
} catch { /* best-effort */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* WS handler: `unify_user_memory_remove` (skeleton).
|
|
85
|
+
*
|
|
86
|
+
* Until 334l lands we have no entries to remove; reply with a noop
|
|
87
|
+
* `user_memory_updated` so the UI can clear its toast.
|
|
88
|
+
*/
|
|
89
|
+
export function handleUnifyUserMemoryRemove(msg, sendUnifyEvent) {
|
|
90
|
+
const send = sendUnifyEvent || _sendUnifyEvent;
|
|
91
|
+
if (!send) return;
|
|
92
|
+
|
|
93
|
+
const requestId = msg && typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
94
|
+
const entryId = msg && typeof msg.entryId === 'string' ? msg.entryId : null;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
send({
|
|
98
|
+
type: 'user_memory_removed',
|
|
99
|
+
entryId,
|
|
100
|
+
pending: true, // 334l will flip once real removal lands
|
|
101
|
+
...(requestId ? { requestId } : {}),
|
|
102
|
+
});
|
|
103
|
+
} catch { /* best-effort */ }
|
|
104
|
+
}
|
package/unify/web-bridge.js
CHANGED
|
@@ -28,6 +28,11 @@ import ctx from '../context.js';
|
|
|
28
28
|
import { getThreadStore, MAIN_THREAD_ID } from './threads/store.js';
|
|
29
29
|
import { handleVpSubscribe } from './vp/vp-bridge.js';
|
|
30
30
|
import { createVp, updateVp, deleteVp, readVp, VpCrudError } from './vp/vp-crud.js';
|
|
31
|
+
import { handleUnifyTaskMessage as _handleUnifyTaskMessage } from './task-message.js';
|
|
32
|
+
import {
|
|
33
|
+
handleUnifyUserMemoryWrite as _handleUnifyUserMemoryWrite,
|
|
34
|
+
handleUnifyUserMemoryRemove as _handleUnifyUserMemoryRemove,
|
|
35
|
+
} from './user-memory.js';
|
|
31
36
|
|
|
32
37
|
/** @type {import('./session.js').Session | null} */
|
|
33
38
|
let session = null;
|
|
@@ -198,6 +203,40 @@ export function handleUnifyVpDelete(msg) {
|
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
|
|
206
|
+
/**
|
|
207
|
+
* task-334h (R6 §Δ28 / §Δ31.6): task-scoped direct message echo.
|
|
208
|
+
*
|
|
209
|
+
* Replaces the withdrawn R3 `unify_task_private_chat`. The agent acts as a
|
|
210
|
+
* relay: validate → stamp msgId + ts → broadcast `task_message`. Real
|
|
211
|
+
* persistence + task ACL lands in 334l.
|
|
212
|
+
*
|
|
213
|
+
* @param {any} msg
|
|
214
|
+
*/
|
|
215
|
+
export function handleUnifyTaskMessage(msg) {
|
|
216
|
+
_handleUnifyTaskMessage(msg, sendUnifyEvent);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* task-334h (R6 §Δ29): user-memory write skeleton. Replies with a
|
|
221
|
+
* `user_memory_updated` ack carrying `pending: true`; 334l replaces the
|
|
222
|
+
* stub with real ingestion + entryId.
|
|
223
|
+
*
|
|
224
|
+
* @param {any} msg
|
|
225
|
+
*/
|
|
226
|
+
export function handleUnifyUserMemoryWrite(msg) {
|
|
227
|
+
_handleUnifyUserMemoryWrite(msg, sendUnifyEvent);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* task-334h (R6 §Δ29): user-memory remove skeleton. Replies with
|
|
232
|
+
* `user_memory_removed` ack; 334l replaces the stub.
|
|
233
|
+
*
|
|
234
|
+
* @param {any} msg
|
|
235
|
+
*/
|
|
236
|
+
export function handleUnifyUserMemoryRemove(msg) {
|
|
237
|
+
_handleUnifyUserMemoryRemove(msg, sendUnifyEvent);
|
|
238
|
+
}
|
|
239
|
+
|
|
201
240
|
export function handleUnifyVpRead(msg) {
|
|
202
241
|
const requestId = msg && msg.requestId;
|
|
203
242
|
const vpId = msg && msg.vpId;
|