@rikaidev/yomi 0.4.2 → 0.6.0

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.
Files changed (86) hide show
  1. package/README.md +46 -4
  2. package/dist/ambient/ambient-event-graph.js +172 -0
  3. package/dist/ambient/ambient-observer.js +71 -0
  4. package/dist/ambient/event-graph-reducer.js +300 -0
  5. package/dist/ambient/ingestion-adapter.js +203 -0
  6. package/dist/ambient/journal/crypto.js +58 -0
  7. package/dist/ambient/journal/journal-store.js +284 -0
  8. package/dist/ambient/journal/types.js +7 -0
  9. package/dist/ambient/summaries/retriever.js +55 -0
  10. package/dist/ambient/summaries/summary-engine.js +128 -0
  11. package/dist/ambient/summaries/types.js +4 -0
  12. package/dist/attention/policy.js +101 -0
  13. package/dist/auth/credential-store.js +47 -17
  14. package/dist/auth/keychain.js +7 -54
  15. package/dist/cli/query.js +875 -0
  16. package/dist/cli/work-item.js +24 -0
  17. package/dist/insight/relationships.js +1 -1
  18. package/dist/line/auth/protocol/pwless-login-flow.js +12 -0
  19. package/dist/line/auth/pwless/index.js +1 -1
  20. package/dist/line/client/auth-service/client.js +7 -8
  21. package/dist/line/client/buddy-service/client.js +73 -0
  22. package/dist/line/client/index.js +28 -3
  23. package/dist/line/client/obs-media-client.js +38 -0
  24. package/dist/line/client/parsers.js +1 -0
  25. package/dist/line/client/square-service/client.js +298 -0
  26. package/dist/line/client/sticker-meta.js +32 -0
  27. package/dist/line/client/sync-service/client.js +31 -3
  28. package/dist/line/client/talk-service/command-client.js +9 -1
  29. package/dist/line/client/talk-service/contact-query.js +8 -1
  30. package/dist/line/client/talk-service/message-box-query.js +17 -0
  31. package/dist/line/client/talk-service/message-id-query.js +27 -3
  32. package/dist/line/client/talk-service/query-client.js +32 -26
  33. package/dist/line/client/talk-service/requests.js +10 -11
  34. package/dist/line/core/auth-session-runtime.js +17 -2
  35. package/dist/line/core/chat-event.js +35 -0
  36. package/dist/line/core/chat-runtime-service.js +18 -23
  37. package/dist/line/core/config.js +1 -0
  38. package/dist/line/core/directory-runtime-service.js +10 -2
  39. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +8 -16
  40. package/dist/line/core/flex-message.js +453 -0
  41. package/dist/line/core/mention.js +1 -1
  42. package/dist/line/core/message-preview.js +60 -0
  43. package/dist/line/core/message-query-service.js +0 -20
  44. package/dist/line/core/service.js +1 -3
  45. package/dist/mcp/handlers/contacts.js +12 -6
  46. package/dist/mcp/handlers/conversations.js +91 -4
  47. package/dist/mcp/handlers/login.js +1 -1
  48. package/dist/mcp/handlers/message-actions.js +1 -1
  49. package/dist/mcp/handlers/search.js +1 -1
  50. package/dist/mcp/handlers/send.js +8 -11
  51. package/dist/mcp/handlers/shared.js +1 -1
  52. package/dist/mcp/integrator.js +168 -0
  53. package/dist/mcp/media.js +135 -4
  54. package/dist/mcp/names.js +78 -6
  55. package/dist/mcp/phi-guard.js +2 -2
  56. package/dist/search/bigram.js +2 -2
  57. package/dist/search/collector.js +1 -1
  58. package/dist/search/default-embedder.js +12 -3
  59. package/dist/search/embedder.js +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/work-item/adapter.js +1 -0
  62. package/dist/work-item/destination-contracts.js +1 -0
  63. package/dist/work-item/event-context.js +73 -0
  64. package/dist/work-item/gold-contract.js +101 -0
  65. package/dist/work-item/gold-coverage.js +96 -0
  66. package/dist/work-item/gold-derivation.js +67 -0
  67. package/dist/work-item/gold-split.js +143 -0
  68. package/dist/work-item/index.js +23 -0
  69. package/dist/work-item/line-adapter.js +111 -0
  70. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  71. package/dist/work-item/local-provider.js +38 -0
  72. package/dist/work-item/local-work-event-provider.js +56 -0
  73. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  74. package/dist/work-item/provider.js +21 -0
  75. package/dist/work-item/registry.js +19 -0
  76. package/dist/work-item/service.js +85 -0
  77. package/dist/work-item/store.js +97 -0
  78. package/dist/work-item/types.js +79 -0
  79. package/dist/work-item/work-event-graph-engine.js +63 -0
  80. package/dist/work-item/work-event-inference.js +267 -0
  81. package/dist/work-item/work-event-matcher.js +105 -0
  82. package/dist/work-item/work-event-model-manifest.js +39 -0
  83. package/dist/work-item/work-event-store.js +125 -0
  84. package/dist/work-item/work-event.js +123 -0
  85. package/package.json +26 -2
  86. package/run.mjs +371 -2
@@ -0,0 +1,143 @@
1
+ function labelsFor(record) {
2
+ return [
3
+ `work:${record.createsWork}`,
4
+ ...record.dialogueActs.map((value) => `dialogue:${value}`),
5
+ `ownership:${record.ownership.state}`,
6
+ ...record.ownership.ownerKinds.map((value) => `owner_kind:${value}`),
7
+ `dependency:${record.dependency}`,
8
+ `lifecycle:${record.lifecycle}`,
9
+ `completion:delivered:${record.completion.delivered}`,
10
+ `completion:accepted:${record.completion.accepted}`,
11
+ `completion:cancelled:${record.completion.cancelled}`,
12
+ ];
13
+ }
14
+ function labelCounts(records) {
15
+ const counts = new Map();
16
+ for (const record of records) {
17
+ for (const label of labelsFor(record))
18
+ counts.set(label, (counts.get(label) ?? 0) + 1);
19
+ }
20
+ return counts;
21
+ }
22
+ function stableHash(value) {
23
+ let hash = 0x811c9dc5;
24
+ for (const character of value) {
25
+ hash ^= character.codePointAt(0) ?? 0;
26
+ hash = Math.imul(hash, 0x01000193);
27
+ }
28
+ return hash >>> 0;
29
+ }
30
+ function conversationId(record) {
31
+ return record.source?.chatId ?? `synthetic:${record.id}`;
32
+ }
33
+ /**
34
+ * Assign whole conversations, never windows, to partitions. Public acceptance
35
+ * fixtures are removed before grouping so they cannot leak into model fitting.
36
+ */
37
+ export function splitFactorizedGoldByConversation(records, options) {
38
+ const acceptance = new Set(options.acceptanceIds ?? []);
39
+ const excludedAcceptanceIds = [];
40
+ const groups = new Map();
41
+ for (const record of records) {
42
+ if (acceptance.has(record.id)) {
43
+ excludedAcceptanceIds.push(record.id);
44
+ continue;
45
+ }
46
+ const key = conversationId(record);
47
+ groups.set(key, [...(groups.get(key) ?? []), record]);
48
+ }
49
+ const validationFraction = options.validationFraction ?? 0.15;
50
+ const testFraction = options.testFraction ?? 0.15;
51
+ if (validationFraction < 0 ||
52
+ testFraction < 0 ||
53
+ validationFraction + testFraction >= 1) {
54
+ throw new Error('Invalid split fractions');
55
+ }
56
+ const validationGroups = groups.size >= 3
57
+ ? Math.max(1, Math.round(groups.size * validationFraction))
58
+ : 0;
59
+ const testGroups = groups.size >= 3 ? Math.max(1, Math.round(groups.size * testFraction)) : 0;
60
+ const trainGroups = Math.max(0, groups.size - validationGroups - testGroups);
61
+ const allRecords = [...groups.values()].flat();
62
+ const totals = labelCounts(allRecords);
63
+ const shares = {
64
+ train: 1 - validationFraction - testFraction,
65
+ validation: validationFraction,
66
+ test: testFraction,
67
+ };
68
+ const capacities = {
69
+ train: trainGroups,
70
+ validation: validationGroups,
71
+ test: testGroups,
72
+ };
73
+ const assigned = {
74
+ train: [],
75
+ validation: [],
76
+ test: [],
77
+ };
78
+ const assignedGroups = { train: 0, validation: 0, test: 0 };
79
+ const current = {
80
+ train: new Map(),
81
+ validation: new Map(),
82
+ test: new Map(),
83
+ };
84
+ const ordered = [...groups.entries()].sort(([leftId, left], [rightId, right]) => {
85
+ const rarity = (records) => [...new Set(records.flatMap(labelsFor))].reduce((sum, label) => sum + 1 / (totals.get(label) ?? 1), 0);
86
+ return (rarity(right) - rarity(left) ||
87
+ stableHash(`${options.seed}:${leftId}`) -
88
+ stableHash(`${options.seed}:${rightId}`));
89
+ });
90
+ for (const [groupId, records] of ordered) {
91
+ const counts = labelCounts(records);
92
+ const candidates = ['train', 'validation', 'test'].filter((name) => assignedGroups[name] < capacities[name]);
93
+ const scored = candidates
94
+ .map((name) => {
95
+ let labelNeed = 0;
96
+ for (const [label, count] of counts) {
97
+ const target = (totals.get(label) ?? 0) * shares[name];
98
+ labelNeed +=
99
+ (count * Math.max(0, target - (current[name].get(label) ?? 0))) /
100
+ Math.max(1, target);
101
+ }
102
+ const targetRecords = allRecords.length * shares[name];
103
+ const sizeNeed = Math.max(0, targetRecords - assigned[name].length) /
104
+ Math.max(1, targetRecords);
105
+ return {
106
+ name,
107
+ score: labelNeed * 4 + sizeNeed,
108
+ tie: stableHash(`${options.seed}:${groupId}:${name}`),
109
+ };
110
+ })
111
+ .sort((left, right) => right.score - left.score || left.tie - right.tie);
112
+ const destination = scored[0]?.name;
113
+ if (!destination)
114
+ throw new Error('No partition has remaining conversation capacity');
115
+ assigned[destination].push(...records);
116
+ assignedGroups[destination] += 1;
117
+ for (const [label, count] of counts)
118
+ current[destination].set(label, (current[destination].get(label) ?? 0) + count);
119
+ }
120
+ return {
121
+ seed: options.seed,
122
+ train: assigned.train,
123
+ validation: assigned.validation,
124
+ test: assigned.test,
125
+ excludedAcceptanceIds: excludedAcceptanceIds.sort(),
126
+ };
127
+ }
128
+ export function assertNoConversationLeakage(split) {
129
+ const seen = new Map();
130
+ for (const [partition, records] of Object.entries({
131
+ train: split.train,
132
+ validation: split.validation,
133
+ test: split.test,
134
+ })) {
135
+ for (const record of records) {
136
+ const key = conversationId(record);
137
+ const previous = seen.get(key);
138
+ if (previous && previous !== partition)
139
+ throw new Error(`Conversation ${key} leaked from ${previous} into ${partition}`);
140
+ seen.set(key, partition);
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,23 @@
1
+ export * from './adapter.js';
2
+ export * from './destination-contracts.js';
3
+ export * from './event-context.js';
4
+ export * from './gold-contract.js';
5
+ export * from './gold-coverage.js';
6
+ export * from './gold-derivation.js';
7
+ export * from './gold-split.js';
8
+ export * from './line-adapter.js';
9
+ export * from './llama-cpp-work-event-adapter.js';
10
+ export * from './local-provider.js';
11
+ export * from './local-work-event-provider.js';
12
+ export * from './onnx-work-event-adapter.js';
13
+ export * from './provider.js';
14
+ export * from './registry.js';
15
+ export * from './service.js';
16
+ export * from './store.js';
17
+ export * from './types.js';
18
+ export * from './work-event.js';
19
+ export * from './work-event-graph-engine.js';
20
+ export * from './work-event-inference.js';
21
+ export * from './work-event-matcher.js';
22
+ export * from './work-event-model-manifest.js';
23
+ export * from './work-event-store.js';
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+ function asBytes(bytes) {
3
+ if (!bytes)
4
+ return undefined;
5
+ return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
6
+ }
7
+ function normalizeAttachment(input, messageId, index) {
8
+ const bytes = asBytes(input.bytes);
9
+ return {
10
+ id: String(input.id ?? `${messageId}:attachment:${index}`),
11
+ fileName: input.fileName ?? null,
12
+ mimeType: input.mimeType ?? input.mediaType ?? null,
13
+ byteLength: input.byteLength ?? input.size ?? bytes?.byteLength ?? null,
14
+ sha256: bytes ? createHash('sha256').update(bytes).digest('hex') : null,
15
+ ...(bytes ? { bytes } : {}),
16
+ ...(input.url || input.mediaUrl
17
+ ? { sourceUrl: input.url ?? input.mediaUrl ?? undefined }
18
+ : {}),
19
+ };
20
+ }
21
+ function messageAttachments(input, messageId) {
22
+ const values = [
23
+ ...(input.attachments ?? []),
24
+ ...(input.attachment ? [input.attachment] : []),
25
+ ];
26
+ if (values.length === 0 && (input.mediaUrl || input.fileName)) {
27
+ values.push({
28
+ fileName: input.fileName,
29
+ mediaType: input.mediaType,
30
+ mediaUrl: input.mediaUrl,
31
+ });
32
+ }
33
+ return values.map((item, index) => normalizeAttachment(item, messageId, index));
34
+ }
35
+ export function isLineWorkItemMessage(input) {
36
+ return (typeof input === 'object' &&
37
+ input !== null &&
38
+ (typeof input.id === 'string' ||
39
+ typeof input.id === 'number'));
40
+ }
41
+ export class LineWorkItemAdapter {
42
+ id = 'line.message';
43
+ platform = 'line';
44
+ canNormalize(input) {
45
+ return (isLineWorkItemMessage(input) ||
46
+ (Array.isArray(input) && input.every(isLineWorkItemMessage)));
47
+ }
48
+ normalize(input) {
49
+ const inputs = Array.isArray(input) ? [...input] : [input];
50
+ if (inputs.length === 0)
51
+ throw new Error('LINE work item requires at least one message');
52
+ const normalized = inputs.map((message) => {
53
+ const messageId = String(message.id);
54
+ const conversationId = String(message.conversationId ?? message.chatId ?? '');
55
+ if (!conversationId)
56
+ throw new Error('LINE work item requires conversationId');
57
+ const sentAt = message.timestamp ?? Date.now();
58
+ const source = {
59
+ adapter: this.id,
60
+ platform: this.platform,
61
+ conversationId,
62
+ messageIds: [messageId],
63
+ latestMessageAt: sentAt,
64
+ };
65
+ return {
66
+ conversationId,
67
+ evidence: {
68
+ source,
69
+ sourceMessageId: messageId,
70
+ senderId: message.senderId ?? message.fromMid ?? null,
71
+ senderName: message.senderName ?? message.fromName ?? null,
72
+ sentAt,
73
+ text: message.text ?? message.content ?? null,
74
+ attachments: messageAttachments(message, messageId),
75
+ },
76
+ };
77
+ });
78
+ const seen = new Set();
79
+ const evidence = normalized
80
+ .sort((a, b) => a.evidence.sentAt - b.evidence.sentAt ||
81
+ a.evidence.sourceMessageId.localeCompare(b.evidence.sourceMessageId))
82
+ .flatMap((message) => {
83
+ const key = `${message.conversationId}:${message.evidence.sourceMessageId}`;
84
+ if (seen.has(key))
85
+ return [];
86
+ seen.add(key);
87
+ return [message.evidence];
88
+ });
89
+ const sourceByConversation = new Map();
90
+ for (const item of evidence) {
91
+ const itemSource = item.source;
92
+ const existing = sourceByConversation.get(itemSource.conversationId);
93
+ if (existing) {
94
+ existing.messageIds.push(...itemSource.messageIds);
95
+ existing.latestMessageAt = Math.max(existing.latestMessageAt, itemSource.latestMessageAt);
96
+ }
97
+ else {
98
+ sourceByConversation.set(itemSource.conversationId, {
99
+ ...itemSource,
100
+ messageIds: [...itemSource.messageIds],
101
+ });
102
+ }
103
+ }
104
+ const sources = [...sourceByConversation.values()];
105
+ return {
106
+ source: sources[0],
107
+ sources,
108
+ evidence,
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,239 @@
1
+ const ACTS = [
2
+ 'inform',
3
+ 'ask',
4
+ 'answer',
5
+ 'directive',
6
+ 'commit',
7
+ 'delegate',
8
+ 'acknowledge',
9
+ 'deliver',
10
+ 'accept',
11
+ 'reject',
12
+ 'cancel',
13
+ ];
14
+ const OWNER_STATES = ['unassigned', 'owned', 'delegated', 'unknown'];
15
+ const PARTICIPANT_KINDS = [
16
+ 'self',
17
+ 'conversation_member',
18
+ 'external_party',
19
+ 'unknown',
20
+ ];
21
+ const DEPENDENCIES = [
22
+ 'none',
23
+ 'waiting_person',
24
+ 'waiting_external_system',
25
+ 'waiting_decision',
26
+ 'unknown',
27
+ ];
28
+ const LIFECYCLES = [
29
+ 'proposed',
30
+ 'committed',
31
+ 'in_progress',
32
+ 'blocked',
33
+ 'delivered',
34
+ 'accepted',
35
+ 'cancelled',
36
+ 'resolved',
37
+ 'uncertain',
38
+ ];
39
+ const evidenceField = {
40
+ type: 'object',
41
+ required: ['confidence', 'evidenceMessageIds'],
42
+ properties: {
43
+ confidence: { type: 'number', minimum: 0, maximum: 1 },
44
+ evidenceMessageIds: { type: 'array', items: { type: 'string' } },
45
+ },
46
+ };
47
+ export const WORK_EVENT_GRAPH_JSON_SCHEMA = {
48
+ type: 'object',
49
+ required: ['dialogue', 'candidates'],
50
+ properties: {
51
+ dialogue: {
52
+ type: 'array',
53
+ items: {
54
+ type: 'object',
55
+ required: ['messageId', 'acts', 'confidence'],
56
+ properties: {
57
+ messageId: { type: 'string' },
58
+ acts: {
59
+ type: 'array',
60
+ minItems: 1,
61
+ items: { type: 'string', enum: ACTS },
62
+ },
63
+ confidence: { type: 'number', minimum: 0, maximum: 1 },
64
+ },
65
+ },
66
+ },
67
+ candidates: {
68
+ type: 'array',
69
+ items: {
70
+ type: 'object',
71
+ required: [
72
+ 'candidateKey',
73
+ 'createsWork',
74
+ 'action',
75
+ 'ownership',
76
+ 'dependency',
77
+ 'lifecycle',
78
+ 'completion',
79
+ ],
80
+ properties: {
81
+ candidateKey: { type: 'string' },
82
+ createsWork: {
83
+ ...evidenceField,
84
+ required: ['value', 'confidence', 'evidenceMessageIds'],
85
+ properties: {
86
+ ...evidenceField.properties,
87
+ value: { type: 'boolean' },
88
+ },
89
+ },
90
+ action: {
91
+ anyOf: [
92
+ { type: 'null' },
93
+ {
94
+ type: 'object',
95
+ required: [
96
+ 'predicate',
97
+ 'object',
98
+ 'expectedOutcome',
99
+ 'dueAt',
100
+ 'sourceMessageIds',
101
+ 'confidence',
102
+ ],
103
+ properties: {
104
+ predicate: { type: 'string' },
105
+ object: { type: ['string', 'null'] },
106
+ expectedOutcome: { type: ['string', 'null'] },
107
+ dueAt: { type: ['number', 'null'] },
108
+ sourceMessageIds: {
109
+ type: 'array',
110
+ minItems: 1,
111
+ items: { type: 'string' },
112
+ },
113
+ confidence: { type: 'number', minimum: 0, maximum: 1 },
114
+ },
115
+ },
116
+ ],
117
+ },
118
+ ownership: {
119
+ ...evidenceField,
120
+ required: ['value', 'confidence', 'evidenceMessageIds'],
121
+ properties: {
122
+ ...evidenceField.properties,
123
+ value: {
124
+ type: 'object',
125
+ required: ['state', 'owners'],
126
+ properties: {
127
+ state: { type: 'string', enum: OWNER_STATES },
128
+ owners: {
129
+ type: 'array',
130
+ items: {
131
+ type: 'object',
132
+ required: ['kind', 'stableId', 'displayName'],
133
+ properties: {
134
+ kind: { type: 'string', enum: PARTICIPANT_KINDS },
135
+ stableId: { type: ['string', 'null'] },
136
+ displayName: { type: ['string', 'null'] },
137
+ },
138
+ },
139
+ },
140
+ },
141
+ },
142
+ },
143
+ },
144
+ dependency: {
145
+ ...evidenceField,
146
+ required: ['value', 'confidence', 'evidenceMessageIds'],
147
+ properties: {
148
+ ...evidenceField.properties,
149
+ value: {
150
+ type: 'object',
151
+ required: ['state', 'on'],
152
+ properties: {
153
+ state: { type: 'string', enum: DEPENDENCIES },
154
+ on: { type: ['object', 'null'] },
155
+ },
156
+ },
157
+ },
158
+ },
159
+ lifecycle: {
160
+ ...evidenceField,
161
+ required: ['value', 'confidence', 'evidenceMessageIds'],
162
+ properties: {
163
+ ...evidenceField.properties,
164
+ value: { type: 'string', enum: LIFECYCLES },
165
+ },
166
+ },
167
+ completion: {
168
+ ...evidenceField,
169
+ required: ['value', 'confidence', 'evidenceMessageIds'],
170
+ properties: {
171
+ ...evidenceField.properties,
172
+ value: {
173
+ type: 'object',
174
+ required: ['delivered', 'accepted', 'cancelled'],
175
+ properties: {
176
+ delivered: { type: 'boolean' },
177
+ accepted: { type: 'boolean' },
178
+ cancelled: { type: 'boolean' },
179
+ },
180
+ },
181
+ },
182
+ },
183
+ },
184
+ },
185
+ },
186
+ },
187
+ };
188
+ const SYSTEM_PROMPT = `Infer a revisable work-event graph from the complete event context. Dialogue acts are message-level. Return zero or more distinct candidates; one message may cancel one action and propose another, so candidates may share evidence. Information and answers can create work. Acknowledgement is not acceptance. Never infer work from read state, names, or account identity. Action frames must cite source message IDs. If uncertain, use low confidence rather than inventing evidence.`;
189
+ export class LlamaCppWorkEventAdapter {
190
+ model;
191
+ request;
192
+ timeoutMs;
193
+ id;
194
+ endpoint;
195
+ constructor(baseUrl, model, request = fetch, timeoutMs = 120_000) {
196
+ this.model = model;
197
+ this.request = request;
198
+ this.timeoutMs = timeoutMs;
199
+ const url = new URL(baseUrl);
200
+ if (url.protocol !== 'http:' ||
201
+ !['127.0.0.1', '::1', '[::1]', 'localhost'].includes(url.hostname)) {
202
+ throw new Error('Work-event model endpoint must be loopback HTTP');
203
+ }
204
+ this.endpoint = new URL('/v1/chat/completions', url);
205
+ this.id = `llama.cpp:${model}`;
206
+ }
207
+ async generate(input) {
208
+ const response = await this.request(this.endpoint, {
209
+ method: 'POST',
210
+ headers: { 'content-type': 'application/json' },
211
+ signal: AbortSignal.timeout(this.timeoutMs),
212
+ body: JSON.stringify({
213
+ model: this.model,
214
+ messages: [
215
+ { role: 'system', content: SYSTEM_PROMPT },
216
+ { role: 'user', content: input },
217
+ ],
218
+ response_format: {
219
+ type: 'json_schema',
220
+ json_schema: {
221
+ name: 'work_event_graph',
222
+ strict: true,
223
+ schema: WORK_EVENT_GRAPH_JSON_SCHEMA,
224
+ },
225
+ },
226
+ temperature: 0,
227
+ max_tokens: 1800,
228
+ }),
229
+ });
230
+ if (!response.ok)
231
+ throw new Error(`Local model request failed (${response.status})`);
232
+ const payload = (await response.json());
233
+ const content = payload?.choices?.[0]?.message?.content;
234
+ if (typeof content !== 'string' || content.length > 1024 * 1024)
235
+ throw new Error('Local model response is missing or oversized');
236
+ const parsed = JSON.parse(content);
237
+ return { ...parsed, modelId: this.id };
238
+ }
239
+ }
@@ -0,0 +1,38 @@
1
+ import { validateProviderCapabilities, } from './provider.js';
2
+ /**
3
+ * A local review sink. It deliberately never opens a socket or invokes a
4
+ * system share panel, so preview/delivery can be exercised safely in tests.
5
+ */
6
+ export class LocalReviewProvider {
7
+ id = 'local.review';
8
+ displayName = 'Local review';
9
+ capabilities = {
10
+ operations: ['share', 'message', 'issue', 'review'],
11
+ attachments: true,
12
+ maxAttachments: 100,
13
+ requiresApproval: true,
14
+ };
15
+ delivered = new Map();
16
+ validate(draft) {
17
+ return validateProviderCapabilities(this, draft);
18
+ }
19
+ async deliver(draft) {
20
+ const validation = this.validate(draft);
21
+ if (!validation.valid)
22
+ throw new Error(validation.errors.join('; '));
23
+ // Keep the in-process object available to a local UI/reviewer only.
24
+ this.delivered.set(draft.id, structuredClone(draft));
25
+ return {
26
+ delivery: {
27
+ providerId: this.id,
28
+ deliveredAt: Date.now(),
29
+ external: false,
30
+ reference: `local-review://${draft.id}`,
31
+ },
32
+ };
33
+ }
34
+ get(id) {
35
+ const draft = this.delivered.get(id);
36
+ return draft ? structuredClone(draft) : null;
37
+ }
38
+ }
@@ -0,0 +1,56 @@
1
+ import { validateFactorizedGraphModelOutput, } from './work-event-inference.js';
2
+ import { assertProductionEligibleModel, } from './work-event-model-manifest.js';
3
+ export class LocalModelOutputError extends Error {
4
+ reasons;
5
+ constructor(reasons) {
6
+ super(`Local work-event model output is invalid: ${reasons.join(', ')}`);
7
+ this.reasons = reasons;
8
+ }
9
+ }
10
+ export function serializeEventContextForModel(context) {
11
+ const messages = context.messages.map((message, index) => JSON.stringify({
12
+ index,
13
+ messageId: message.messageId,
14
+ speaker: message.isSelf ? 'self' : 'other',
15
+ sentAt: message.sentAt,
16
+ text: message.text,
17
+ mediaType: message.mediaType ?? null,
18
+ mentionsSelf: message.mentionsSelf ?? false,
19
+ }));
20
+ const edges = context.edges.map((edge) => JSON.stringify({
21
+ parentId: edge.parentId,
22
+ childId: edge.childId,
23
+ weight: edge.weight,
24
+ entropy: edge.entropy,
25
+ }));
26
+ return [
27
+ JSON.stringify({
28
+ schemaVersion: 1,
29
+ chatId: context.chatId,
30
+ anchorMessageIds: context.anchorMessageIds,
31
+ complete: context.complete,
32
+ boundary: context.boundary,
33
+ }),
34
+ ...messages,
35
+ 'EDGES',
36
+ ...edges,
37
+ ].join('\n');
38
+ }
39
+ export class LocalFactorizedWorkEventProvider {
40
+ adapter;
41
+ id;
42
+ constructor(adapter, manifest) {
43
+ this.adapter = adapter;
44
+ assertProductionEligibleModel(manifest);
45
+ if (manifest.modelId !== adapter.id)
46
+ throw new Error('Model manifest does not match the local adapter');
47
+ this.id = `local:${adapter.id}`;
48
+ }
49
+ async infer(context) {
50
+ const raw = await this.adapter.generate(serializeEventContextForModel(context));
51
+ const reasons = validateFactorizedGraphModelOutput(raw);
52
+ if (reasons.length > 0)
53
+ throw new LocalModelOutputError(reasons);
54
+ return { ...raw, modelId: this.id };
55
+ }
56
+ }