@rikaidev/yomi 0.5.0 → 0.6.1

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 (63) hide show
  1. package/README.md +28 -0
  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 +22 -16
  13. package/dist/auth/credential-store.js +25 -12
  14. package/dist/cli/query.js +276 -69
  15. package/dist/cli/work-item.js +24 -0
  16. package/dist/line/client/auth-service/client.js +7 -8
  17. package/dist/line/client/buddy-service/client.js +73 -0
  18. package/dist/line/client/index.js +28 -1
  19. package/dist/line/client/obs-media-client.js +2 -1
  20. package/dist/line/client/square-service/client.js +298 -0
  21. package/dist/line/client/talk-service/command-client.js +9 -1
  22. package/dist/line/client/talk-service/message-id-query.js +27 -3
  23. package/dist/line/client/talk-service/requests.js +9 -0
  24. package/dist/line/core/auth-session-runtime.js +1 -1
  25. package/dist/line/core/chat-event.js +2 -0
  26. package/dist/line/core/chat-runtime-service.js +18 -3
  27. package/dist/line/core/config.js +1 -0
  28. package/dist/line/core/flex-message.js +323 -0
  29. package/dist/line/core/service.js +0 -1
  30. package/dist/mcp/handlers/contacts.js +12 -6
  31. package/dist/mcp/handlers/conversations.js +54 -0
  32. package/dist/mcp/handlers/send.js +8 -11
  33. package/dist/mcp/integrator.js +21 -7
  34. package/dist/mcp/media.js +101 -2
  35. package/dist/mcp/names.js +78 -6
  36. package/dist/search/default-embedder.js +12 -3
  37. package/dist/version.js +1 -1
  38. package/dist/work-item/adapter.js +1 -0
  39. package/dist/work-item/destination-contracts.js +1 -0
  40. package/dist/work-item/event-context.js +73 -0
  41. package/dist/work-item/gold-contract.js +101 -0
  42. package/dist/work-item/gold-coverage.js +96 -0
  43. package/dist/work-item/gold-derivation.js +67 -0
  44. package/dist/work-item/gold-split.js +143 -0
  45. package/dist/work-item/index.js +23 -0
  46. package/dist/work-item/line-adapter.js +111 -0
  47. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  48. package/dist/work-item/local-provider.js +38 -0
  49. package/dist/work-item/local-work-event-provider.js +56 -0
  50. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  51. package/dist/work-item/provider.js +21 -0
  52. package/dist/work-item/registry.js +19 -0
  53. package/dist/work-item/service.js +85 -0
  54. package/dist/work-item/store.js +97 -0
  55. package/dist/work-item/types.js +79 -0
  56. package/dist/work-item/work-event-graph-engine.js +63 -0
  57. package/dist/work-item/work-event-inference.js +267 -0
  58. package/dist/work-item/work-event-matcher.js +105 -0
  59. package/dist/work-item/work-event-model-manifest.js +39 -0
  60. package/dist/work-item/work-event-store.js +125 -0
  61. package/dist/work-item/work-event.js +123 -0
  62. package/package.json +25 -4
  63. package/run.mjs +186 -9
@@ -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
+ }
@@ -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
+ }