@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,223 @@
1
+ import { AutoModel, AutoTokenizer } from '@huggingface/transformers';
2
+ const ACTS = [
3
+ 'inform',
4
+ 'ask',
5
+ 'answer',
6
+ 'directive',
7
+ 'commit',
8
+ 'delegate',
9
+ 'acknowledge',
10
+ 'deliver',
11
+ 'accept',
12
+ 'reject',
13
+ 'cancel',
14
+ ];
15
+ const OWNER_STATES = ['unassigned', 'owned', 'delegated', 'unknown'];
16
+ const DEPENDENCIES = [
17
+ 'none',
18
+ 'waiting_person',
19
+ 'waiting_external_system',
20
+ 'waiting_decision',
21
+ 'unknown',
22
+ ];
23
+ const LIFECYCLES = [
24
+ 'proposed',
25
+ 'committed',
26
+ 'in_progress',
27
+ 'blocked',
28
+ 'delivered',
29
+ 'accepted',
30
+ 'cancelled',
31
+ 'resolved',
32
+ 'uncertain',
33
+ ];
34
+ function sigmoid(x) {
35
+ return 1 / (1 + Math.exp(-x));
36
+ }
37
+ function softmax(logits) {
38
+ const max = Math.max(...logits);
39
+ const exp = logits.map((val) => Math.exp(val - max));
40
+ const sum = exp.reduce((acc, val) => acc + val, 0);
41
+ return exp.map((val) => val / sum);
42
+ }
43
+ function argmax(values) {
44
+ let bestIdx = 0;
45
+ let bestVal = values[0];
46
+ for (let i = 1; i < values.length; i++) {
47
+ if (values[i] > bestVal) {
48
+ bestVal = values[i];
49
+ bestIdx = i;
50
+ }
51
+ }
52
+ return bestIdx;
53
+ }
54
+ export class OnnxWorkEventAdapter {
55
+ modelDir;
56
+ id;
57
+ tokenizer = null;
58
+ model = null;
59
+ loadPromise = null;
60
+ constructor(modelDir = './research/models/student-a100-v1', id = 'onnx-student-a100-v1') {
61
+ this.modelDir = modelDir;
62
+ this.id = id;
63
+ }
64
+ async ensureLoaded() {
65
+ if (this.model && this.tokenizer)
66
+ return;
67
+ if (!this.loadPromise) {
68
+ this.loadPromise = (async () => {
69
+ this.tokenizer = await AutoTokenizer.from_pretrained(this.modelDir);
70
+ this.model = await AutoModel.from_pretrained(this.modelDir, {
71
+ local_files_only: true,
72
+ });
73
+ })();
74
+ }
75
+ await this.loadPromise;
76
+ }
77
+ async generate(serializedInput) {
78
+ await this.ensureLoaded();
79
+ // Extract target message and context from serializedInput
80
+ const lines = serializedInput.split('\n');
81
+ const messages = [];
82
+ for (const line of lines) {
83
+ if (line === 'EDGES' || !line.trim())
84
+ break;
85
+ try {
86
+ const parsed = JSON.parse(line);
87
+ if (typeof parsed.index === 'number' && parsed.messageId) {
88
+ messages.push(parsed);
89
+ }
90
+ }
91
+ catch {
92
+ // Skip header or non-message lines
93
+ }
94
+ }
95
+ if (messages.length === 0) {
96
+ return {
97
+ dialogue: [],
98
+ candidates: [],
99
+ modelId: this.id,
100
+ };
101
+ }
102
+ const targetMsg = messages[messages.length - 1];
103
+ const turns = messages.map((m, idx) => {
104
+ const marker = idx === messages.length - 1 ? 'TARGET' : 'CONTEXT';
105
+ const text = m.text || (m.mediaType ? `[${m.mediaType}]` : '');
106
+ return `[${marker} speaker=${m.speaker}] ${text}`;
107
+ });
108
+ const prompt = turns.join('\n');
109
+ const inputs = await this.tokenizer(prompt);
110
+ const outputs = await this.model(inputs);
111
+ // Parse creates_work
112
+ const createsWorkLogit = Number(outputs.creates_work.data[0]);
113
+ const createsWorkConf = sigmoid(createsWorkLogit);
114
+ const createsWork = createsWorkConf >= 0.5;
115
+ // Parse dialogue acts
116
+ const dialogueActsLogits = Array.from(outputs.dialogue.data);
117
+ const detectedActs = [];
118
+ let maxActIdx = 0;
119
+ let maxActVal = -Infinity;
120
+ for (let i = 0; i < dialogueActsLogits.length; i++) {
121
+ const conf = sigmoid(dialogueActsLogits[i]);
122
+ if (conf >= 0.5)
123
+ detectedActs.push(ACTS[i]);
124
+ if (dialogueActsLogits[i] > maxActVal) {
125
+ maxActVal = dialogueActsLogits[i];
126
+ maxActIdx = i;
127
+ }
128
+ }
129
+ if (detectedActs.length === 0)
130
+ detectedActs.push(ACTS[maxActIdx]);
131
+ // Parse ownership
132
+ const ownershipLogits = Array.from(outputs.ownership.data);
133
+ const ownershipProbs = softmax(ownershipLogits);
134
+ const ownIdx = argmax(ownershipProbs);
135
+ const ownState = OWNER_STATES[ownIdx];
136
+ const ownConfidence = ownershipProbs[ownIdx];
137
+ const ownerKinds = ownState === 'owned'
138
+ ? [targetMsg.speaker === 'self' ? 'self' : 'conversation_member']
139
+ : ownState === 'delegated'
140
+ ? ['external_party']
141
+ : ['unknown'];
142
+ // Parse dependency
143
+ const dependencyLogits = Array.from(outputs.dependency.data);
144
+ const dependencyProbs = softmax(dependencyLogits);
145
+ const depIdx = argmax(dependencyProbs);
146
+ const depState = DEPENDENCIES[depIdx];
147
+ const depConfidence = dependencyProbs[depIdx];
148
+ // Parse lifecycle
149
+ const lifecycleLogits = Array.from(outputs.lifecycle.data);
150
+ const lifecycleProbs = softmax(lifecycleLogits);
151
+ const lifeIdx = argmax(lifecycleProbs);
152
+ const lifeState = LIFECYCLES[lifeIdx];
153
+ const lifeConfidence = lifecycleProbs[lifeIdx];
154
+ // Parse completion
155
+ const completionLogits = Array.from(outputs.completion.data);
156
+ const delivered = sigmoid(completionLogits[0]) >= 0.5;
157
+ const accepted = sigmoid(completionLogits[1]) >= 0.5;
158
+ const cancelled = sigmoid(completionLogits[2]) >= 0.5;
159
+ const candidate = {
160
+ candidateKey: targetMsg.messageId,
161
+ createsWork: {
162
+ value: createsWork,
163
+ confidence: createsWork ? createsWorkConf : 1 - createsWorkConf,
164
+ evidenceMessageIds: [targetMsg.messageId],
165
+ },
166
+ action: createsWork
167
+ ? {
168
+ predicate: 'handle',
169
+ object: targetMsg.text?.slice(0, 40) ?? null,
170
+ expectedOutcome: 'task completion',
171
+ dueAt: null,
172
+ sourceMessageIds: [targetMsg.messageId],
173
+ confidence: createsWorkConf,
174
+ }
175
+ : null,
176
+ ownership: {
177
+ value: {
178
+ state: ownState,
179
+ owners: ownerKinds.map((k) => ({
180
+ kind: k,
181
+ stableId: null,
182
+ displayName: null,
183
+ })),
184
+ },
185
+ confidence: ownConfidence,
186
+ evidenceMessageIds: [targetMsg.messageId],
187
+ },
188
+ dependency: {
189
+ value: {
190
+ state: depState,
191
+ on: null,
192
+ },
193
+ confidence: depConfidence,
194
+ evidenceMessageIds: [targetMsg.messageId],
195
+ },
196
+ lifecycle: {
197
+ value: lifeState,
198
+ confidence: lifeConfidence,
199
+ evidenceMessageIds: [targetMsg.messageId],
200
+ },
201
+ completion: {
202
+ value: {
203
+ delivered,
204
+ accepted,
205
+ cancelled,
206
+ },
207
+ confidence: Math.max(sigmoid(completionLogits[0]), sigmoid(completionLogits[1]), sigmoid(completionLogits[2])),
208
+ evidenceMessageIds: [targetMsg.messageId],
209
+ },
210
+ };
211
+ return {
212
+ dialogue: [
213
+ {
214
+ messageId: targetMsg.messageId,
215
+ acts: detectedActs,
216
+ confidence: sigmoid(maxActVal),
217
+ },
218
+ ],
219
+ candidates: [candidate],
220
+ modelId: this.id,
221
+ };
222
+ }
223
+ }
@@ -0,0 +1,21 @@
1
+ export function validateProviderCapabilities(provider, draft) {
2
+ const errors = [];
3
+ if (!provider.capabilities.operations.includes(draft.action)) {
4
+ errors.push(`Provider ${provider.id} does not support ${draft.action}`);
5
+ }
6
+ const attachmentCount = draft.evidence.reduce((count, evidence) => count + evidence.attachments.length, 0);
7
+ if (!provider.capabilities.attachments && attachmentCount > 0) {
8
+ errors.push(`Provider ${provider.id} does not support attachments`);
9
+ }
10
+ else if (attachmentCount > provider.capabilities.maxAttachments) {
11
+ errors.push(`Provider ${provider.id} accepts at most ${provider.capabilities.maxAttachments} attachments`);
12
+ }
13
+ // The service passes `sending` after atomically recording approval. A
14
+ // direct provider call still requires the draft to be approved first.
15
+ if (provider.capabilities.requiresApproval &&
16
+ draft.status !== 'approved' &&
17
+ draft.status !== 'sending') {
18
+ errors.push(`Provider ${provider.id} requires an approved work item`);
19
+ }
20
+ return { valid: errors.length === 0, errors };
21
+ }
@@ -0,0 +1,19 @@
1
+ export class WorkItemProviderRegistry {
2
+ providers = new Map();
3
+ register(provider) {
4
+ if (this.providers.has(provider.id)) {
5
+ throw new Error(`Work item provider already registered: ${provider.id}`);
6
+ }
7
+ this.providers.set(provider.id, provider);
8
+ return this;
9
+ }
10
+ resolve(id) {
11
+ const provider = this.providers.get(id);
12
+ if (!provider)
13
+ throw new Error(`Unknown work item provider: ${id}`);
14
+ return provider;
15
+ }
16
+ list() {
17
+ return [...this.providers.values()];
18
+ }
19
+ }
@@ -0,0 +1,85 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { validateProviderCapabilities, } from './provider.js';
3
+ import { createWorkItemDraft, transitionWorkItem, } from './types.js';
4
+ export class WorkItemService {
5
+ store;
6
+ registry;
7
+ constructor(store, registry) {
8
+ this.store = store;
9
+ this.registry = registry;
10
+ }
11
+ async create(adapter, input, options = {}) {
12
+ const normalized = await adapter.normalize(input);
13
+ const now = options.now ?? Date.now();
14
+ const draft = createWorkItemDraft({
15
+ id: options.id ?? `work-${now}-${randomUUID()}`,
16
+ createdAt: now,
17
+ source: normalized.source,
18
+ sources: normalized.sources,
19
+ evidence: normalized.evidence,
20
+ summary: options.summary ?? null,
21
+ action: options.action ?? 'review',
22
+ destination: options.destination ?? null,
23
+ assignee: options.assignee ?? null,
24
+ labels: options.labels ?? [],
25
+ });
26
+ await this.store.save(draft);
27
+ return draft;
28
+ }
29
+ async transition(id, event) {
30
+ const current = await this.store.get(id);
31
+ if (!current || !('evidence' in current))
32
+ throw new Error(`Unknown work item: ${id}`);
33
+ const next = transitionWorkItem(current, event);
34
+ await this.store.save(next);
35
+ return next;
36
+ }
37
+ async deliver(id, provider) {
38
+ const current = await this.store.get(id);
39
+ if (!current || !('evidence' in current))
40
+ throw new Error(`Unknown work item: ${id}`);
41
+ if (current.destination && current.destination !== provider.id) {
42
+ throw new Error(`Work item destination ${current.destination} does not match provider ${provider.id}`);
43
+ }
44
+ const candidate = current.destination
45
+ ? current
46
+ : { ...current, destination: provider.id };
47
+ const capabilityValidation = validateProviderCapabilities(provider, candidate);
48
+ const providerValidation = provider.validate(candidate);
49
+ const errors = [
50
+ ...capabilityValidation.errors,
51
+ ...providerValidation.errors,
52
+ ];
53
+ if (errors.length > 0)
54
+ throw new Error(errors.join('; '));
55
+ const sending = transitionWorkItem(candidate, { type: 'send' });
56
+ await this.store.save(sending);
57
+ try {
58
+ const result = await provider.deliver(sending);
59
+ const delivered = transitionWorkItem(sending, {
60
+ type: 'deliver',
61
+ delivery: result.delivery,
62
+ });
63
+ await this.store.save(delivered);
64
+ return delivered;
65
+ }
66
+ catch (error) {
67
+ const failed = transitionWorkItem(sending, {
68
+ type: 'fail',
69
+ failure: {
70
+ code: 'provider_delivery_failed',
71
+ message: error instanceof Error ? error.message : String(error),
72
+ at: Date.now(),
73
+ retryable: true,
74
+ },
75
+ });
76
+ await this.store.save(failed);
77
+ throw error;
78
+ }
79
+ }
80
+ async deliverByProviderId(id, providerId) {
81
+ if (!this.registry)
82
+ throw new Error('Work item provider registry is not configured');
83
+ return this.deliver(id, this.registry.resolve(providerId));
84
+ }
85
+ }
@@ -0,0 +1,97 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { yomiDataPath } from '../util/data-dir.js';
5
+ function digest(value) {
6
+ if (value === null)
7
+ return null;
8
+ return createHash('sha256').update(value).digest('hex');
9
+ }
10
+ export function toPersistedWorkItem(draft) {
11
+ const primarySource = draft.source ?? draft.sources[0];
12
+ if (!primarySource)
13
+ throw new Error('Work item has no source to persist');
14
+ return {
15
+ id: draft.id,
16
+ createdAt: draft.createdAt,
17
+ updatedAt: draft.updatedAt,
18
+ source: {
19
+ adapter: primarySource.adapter,
20
+ platform: primarySource.platform,
21
+ conversationIdHash: digest(primarySource.conversationId),
22
+ messageIdHashes: primarySource.messageIds.map((messageId) => digest(messageId)),
23
+ latestMessageAt: primarySource.latestMessageAt,
24
+ },
25
+ evidence: draft.evidence.map((evidence) => ({
26
+ sourceMessageIdHash: digest(evidence.sourceMessageId),
27
+ senderIdHash: evidence.senderId === null ? null : digest(evidence.senderId),
28
+ senderNameHash: evidence.senderName === null ? null : digest(evidence.senderName),
29
+ sentAt: evidence.sentAt,
30
+ textSha256: digest(evidence.text),
31
+ attachments: evidence.attachments.map((attachment) => ({
32
+ idHash: digest(attachment.id),
33
+ fileNameSha256: digest(attachment.fileName),
34
+ mimeType: attachment.mimeType,
35
+ byteLength: attachment.byteLength,
36
+ sha256: attachment.sha256,
37
+ })),
38
+ })),
39
+ summarySha256: digest(draft.summary),
40
+ action: draft.action,
41
+ destination: draft.destination,
42
+ assignee: draft.assignee,
43
+ labels: [...draft.labels],
44
+ status: draft.status,
45
+ failure: draft.failure,
46
+ delivery: draft.delivery,
47
+ };
48
+ }
49
+ export class InMemoryWorkItemStore {
50
+ drafts = new Map();
51
+ async save(draft) {
52
+ this.drafts.set(draft.id, structuredClone(draft));
53
+ }
54
+ async get(id) {
55
+ const draft = this.drafts.get(id);
56
+ return draft ? structuredClone(draft) : null;
57
+ }
58
+ async list() {
59
+ return [...this.drafts.values()].map((draft) => structuredClone(draft));
60
+ }
61
+ }
62
+ /** Metadata-only persistence. It intentionally cannot be used to deliver a draft. */
63
+ export class FileWorkItemStore {
64
+ path;
65
+ maxItems;
66
+ constructor(path = yomiDataPath('work-items.json'), maxItems = 200) {
67
+ this.path = path;
68
+ this.maxItems = Math.max(1, maxItems);
69
+ }
70
+ async save(draft) {
71
+ const entries = await this.read();
72
+ const persisted = toPersistedWorkItem(draft);
73
+ const next = [
74
+ persisted,
75
+ ...entries.filter((entry) => entry.id !== draft.id),
76
+ ].slice(0, this.maxItems);
77
+ await mkdir(dirname(this.path), { recursive: true });
78
+ const temporary = `${this.path}.tmp-${process.pid}`;
79
+ await writeFile(temporary, JSON.stringify(next), { mode: 0o600 });
80
+ await rename(temporary, this.path);
81
+ }
82
+ async get(id) {
83
+ return (await this.read()).find((entry) => entry.id === id) ?? null;
84
+ }
85
+ async list() {
86
+ return this.read();
87
+ }
88
+ async read() {
89
+ try {
90
+ const value = JSON.parse(await readFile(this.path, 'utf8'));
91
+ return Array.isArray(value) ? value : [];
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Destination-agnostic work item domain.
3
+ *
4
+ * Message text and attachment bytes are intentionally kept in memory only.
5
+ * The persistence projection (see store.ts) contains metadata and digests,
6
+ * never decrypted LINE content.
7
+ */
8
+ const TRANSITIONS = {
9
+ draft: ['preview'],
10
+ preview: ['approved', 'draft'],
11
+ approved: ['sending', 'draft'],
12
+ sending: ['delivered', 'failed'],
13
+ delivered: [],
14
+ failed: ['approved', 'draft'],
15
+ };
16
+ export function canTransition(from, to) {
17
+ return TRANSITIONS[from].includes(to);
18
+ }
19
+ export function transitionWorkItem(draft, event, now = Date.now()) {
20
+ const target = event.type === 'preview'
21
+ ? 'preview'
22
+ : event.type === 'approve'
23
+ ? 'approved'
24
+ : event.type === 'send'
25
+ ? 'sending'
26
+ : event.type === 'deliver'
27
+ ? 'delivered'
28
+ : event.type === 'fail'
29
+ ? 'failed'
30
+ : 'approved';
31
+ if (!canTransition(draft.status, target)) {
32
+ throw new Error(`Invalid work item transition: ${draft.status} → ${target}`);
33
+ }
34
+ if (draft.status === 'failed' && event.type !== 'retry') {
35
+ throw new Error('Failed work items must use retry');
36
+ }
37
+ if (event.type === 'retry' && !draft.failure?.retryable) {
38
+ throw new Error('Work item failure is not retryable');
39
+ }
40
+ const updatedAt = event.at ?? now;
41
+ return {
42
+ ...draft,
43
+ status: target,
44
+ updatedAt,
45
+ failure: event.type === 'fail' || event.type === 'retry'
46
+ ? event.type === 'fail'
47
+ ? event.failure
48
+ : null
49
+ : null,
50
+ delivery: event.type === 'deliver' ? event.delivery : draft.delivery,
51
+ };
52
+ }
53
+ export function createWorkItemDraft(input) {
54
+ const sources = input.sources?.length
55
+ ? input.sources
56
+ : input.evidence
57
+ .map((evidence) => evidence.source ?? input.source)
58
+ .filter((source) => Boolean(source));
59
+ return {
60
+ ...input,
61
+ sources,
62
+ source: input.source ?? sources[0],
63
+ updatedAt: input.updatedAt ?? input.createdAt,
64
+ status: 'draft',
65
+ failure: null,
66
+ delivery: null,
67
+ };
68
+ }
69
+ /** Upgrade a draft received from a pre-collection bridge. */
70
+ export function normalizeWorkItemDraft(draft) {
71
+ const sources = draft.sources?.length
72
+ ? draft.sources
73
+ : draft.evidence
74
+ .map((evidence) => evidence.source ?? draft.source)
75
+ .filter((source) => Boolean(source));
76
+ if (sources.length === 0)
77
+ throw new Error('Work item has no valid evidence source');
78
+ return { ...draft, sources, source: draft.source ?? sources[0] };
79
+ }
@@ -0,0 +1,63 @@
1
+ import { reviseWorkEvent, } from './work-event.js';
2
+ import { projectFactorizedGraphInference, } from './work-event-inference.js';
3
+ import { matchWorkEventRevisions } from './work-event-matcher.js';
4
+ export class WorkEventGraphEngine {
5
+ provider;
6
+ sink;
7
+ latest = new Map();
8
+ constructor(provider, sink, initial = []) {
9
+ this.provider = provider;
10
+ this.sink = sink;
11
+ for (const revision of initial)
12
+ this.latest.set(revision.eventId, revision);
13
+ }
14
+ async observe(context, thresholds, inferredAt = Date.now()) {
15
+ let output;
16
+ try {
17
+ output = await this.provider.infer(context);
18
+ }
19
+ catch {
20
+ return {
21
+ projection: {
22
+ dialogue: [],
23
+ candidates: [
24
+ {
25
+ candidateKey: '',
26
+ result: {
27
+ kind: 'abstain',
28
+ reasons: ['provider_inference_failed'],
29
+ modelId: this.provider.id,
30
+ },
31
+ },
32
+ ],
33
+ modelId: this.provider.id,
34
+ },
35
+ revisions: [],
36
+ };
37
+ }
38
+ const projection = projectFactorizedGraphInference(context, output, thresholds, inferredAt);
39
+ const incoming = projection.candidates.flatMap((candidate) => candidate.result.kind === 'inference' ? [candidate.result.inference] : []);
40
+ const previous = [...this.latest.values()].filter((revision) => revision.chatId === context.chatId);
41
+ const matches = matchWorkEventRevisions(previous, incoming);
42
+ const previousByIncoming = new Map(matches.map((match) => [
43
+ match.incomingIndex,
44
+ previous[match.previousIndex],
45
+ ]));
46
+ const revisions = incoming.map((inference, index) => {
47
+ const prior = previousByIncoming.get(index) ?? null;
48
+ const stableInference = prior
49
+ ? { ...inference, eventId: prior.eventId }
50
+ : inference;
51
+ return reviseWorkEvent(prior, stableInference);
52
+ });
53
+ if (revisions.length > 0) {
54
+ await this.sink.appendMany(revisions);
55
+ for (const revision of revisions)
56
+ this.latest.set(revision.eventId, revision);
57
+ }
58
+ return { projection, revisions };
59
+ }
60
+ current() {
61
+ return [...this.latest.values()].sort((left, right) => right.inferredAt - left.inferredAt);
62
+ }
63
+ }