fuli-context 0.7.3 → 0.7.4-beta.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.
- package/README.md +170 -18
- package/README.zh-CN.md +146 -12
- package/compose.graphiti.yml +2 -0
- package/dist/web/assets/{AboutPage-qeO_vm0T.js → AboutPage-CftRexSY.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-DO0GR9Xg.js → ConnectionsPage-D5J91x6d.js} +1 -1
- package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
- package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
- package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
- package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
- package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
- package/dist/web/assets/{OverviewPage-CmCcDCAs.js → OverviewPage-CHE6XhdS.js} +1 -1
- package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
- package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
- package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
- package/dist/web/assets/{ReviewPage-B3vnZ6gn.js → ReviewPage-DUeyu7E8.js} +1 -1
- package/dist/web/assets/{SearchableSelect-ih69qbcS.js → SearchableSelect-C5RiB5F8.js} +1 -1
- package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
- package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
- package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
- package/dist/web/assets/{console-DE40hXGC.js → console-DMvHxKf9.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
- package/dist/web/assets/{i18n-CDxBs1sz.js → i18n-CWfd8Dtt.js} +1 -1
- package/dist/web/assets/{identity-BCxb1mcy.js → identity-C9mLdR8L.js} +1 -1
- package/dist/web/assets/index-F5e2sQ7p.js +2 -0
- package/dist/web/assets/{index-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
- package/dist/web/assets/{model-CCMdt10Q.js → model-CsYAR8O3.js} +1 -1
- package/dist/web/assets/paths-CDREouhi.js +1 -0
- package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
- package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
- package/dist/web/index.html +7 -6
- package/graph-provider/fuli_graph/app.py +238 -2
- package/graph-provider/fuli_graph/auth.py +18 -0
- package/graph-provider/fuli_graph/collaboration_context.py +253 -17
- package/graph-provider/fuli_graph/common_knowledge.py +6 -2
- package/graph-provider/fuli_graph/config.py +5 -0
- package/graph-provider/fuli_graph/graph_models.py +6 -0
- package/graph-provider/fuli_graph/graph_projection.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +27 -3
- package/graph-provider/fuli_graph/knowledge_management.py +19 -1
- package/graph-provider/fuli_graph/knowledge_search.py +11 -0
- package/graph-provider/fuli_graph/models.py +15 -2
- package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
- package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
- package/graph-provider/fuli_graph/project_action_models.py +22 -0
- package/graph-provider/fuli_graph/project_knowledge.py +121 -2
- package/graph-provider/fuli_graph/provider_values.py +24 -0
- package/graph-provider/fuli_graph/runtime.py +78 -0
- package/graph-provider/fuli_graph/search_projection.py +14 -1
- package/graph-provider/fuli_graph/store_knowledge.py +116 -2
- package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
- package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
- package/graph-provider/fuli_graph/workflow_observations.py +84 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +4 -4
- package/skills/capturing-session-knowledge/SKILL.md +32 -5
- package/skills/flreview/SKILL.md +4 -3
- package/src/agent-tools/graph-definitions.js +136 -20
- package/src/agent-tools/graph-handlers.js +10 -4
- package/src/cli/command-registry.js +1 -1
- package/src/cli/local-runtime-command.js +69 -2
- package/src/graphiti/agent-knowledge-workflows.js +497 -16
- package/src/graphiti/collaboration-preference-projection.js +162 -0
- package/src/graphiti/federated-application.js +155 -161
- package/src/graphiti/federated-search-support.js +102 -0
- package/src/graphiti/knowledge-provider-mapping.js +15 -0
- package/src/graphiti/provider-client.js +162 -10
- package/src/graphiti/related-project-suggestions.js +109 -0
- package/src/graphiti/runtime-config.js +14 -0
- package/src/graphiti/source-marker.js +2 -2
- package/src/graphiti/user-taste-skill.js +241 -0
- package/src/graphiti/workflow-candidate-mapping.js +87 -0
- package/src/graphiti/workflow-observation.js +82 -0
- package/src/graphiti/writing-taste-profile-workflow.js +25 -0
- package/src/graphiti/writing-taste-profile.js +490 -0
- package/src/http/graph-api-router.js +15 -2
- package/src/mcp/context-resources.js +158 -0
- package/src/mcp/create-mcp-server.js +39 -5
- package/src/mcp/instructions.js +5 -7
- package/src/mcp/session-id.js +60 -0
- package/src/mcp/task-context-registry.js +15 -3
- package/src/mcp/tool-annotations.js +8 -1
- package/src/mcp/tool-result.js +28 -4
- package/src/server.js +0 -1
- package/src/setup/claude-code-config.js +17 -6
- package/src/setup/graph-runtime.js +20 -2
- package/src/system/runtime-settings.js +121 -2
- package/src/system/system-service.js +5 -1
- package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +0 -1
- package/dist/web/assets/KnowledgePage-BAT4aZIm.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +0 -4
- package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +0 -1
- package/dist/web/assets/PublicProjectsPage-egKBulOq.js +0 -1
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
- package/dist/web/assets/SettingsPage-oLE8mNs5.js +0 -1
- package/dist/web/assets/index-BzhNfzU_.js +0 -2
- package/dist/web/assets/paths-DwHeiGMT.js +0 -1
- package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +0 -1
- package/dist/web/assets/vue-router-Drfl1F8i.js +0 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ITEM_LIMIT = 64;
|
|
4
|
+
const DEFAULT_RECOMMENDATION_LIMIT = 12;
|
|
5
|
+
|
|
6
|
+
const ASPECT_LABELS = Object.freeze({
|
|
7
|
+
taste: 'Taste',
|
|
8
|
+
personality: 'Personality',
|
|
9
|
+
judgment_preference: 'Judgment preferences'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const STATUS_LABELS = Object.freeze({
|
|
13
|
+
confirmed: 'Confirmed',
|
|
14
|
+
agent_confirmed: 'Observed',
|
|
15
|
+
pending: 'Working hypothesis'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function buildUserTasteSkill({
|
|
19
|
+
preferences = [],
|
|
20
|
+
taskPrompt = null,
|
|
21
|
+
personalSpaceId = null,
|
|
22
|
+
personalProjectId = null,
|
|
23
|
+
generatedAt = new Date().toISOString(),
|
|
24
|
+
maxItems = DEFAULT_ITEM_LIMIT,
|
|
25
|
+
recommendationLimit = DEFAULT_RECOMMENDATION_LIMIT
|
|
26
|
+
} = {}) {
|
|
27
|
+
if (!Array.isArray(preferences)) {
|
|
28
|
+
throw new TypeError('User taste preferences must be an array');
|
|
29
|
+
}
|
|
30
|
+
const normalized = preferences
|
|
31
|
+
.map((item, index) => normalizePreference(item, index))
|
|
32
|
+
.filter((item) => item.instruction && item.preferenceKey);
|
|
33
|
+
const active = normalized.filter((item) => item.confirmationStatus !== 'pending');
|
|
34
|
+
const bounded = active.slice(0, boundedLimit(maxItems, DEFAULT_ITEM_LIMIT));
|
|
35
|
+
const recommendations = recommendUserTaste(
|
|
36
|
+
bounded,
|
|
37
|
+
taskPrompt,
|
|
38
|
+
recommendationLimit
|
|
39
|
+
);
|
|
40
|
+
const sourceFingerprint = createHash('sha256')
|
|
41
|
+
.update(canonicalJson(active.map(preferenceFingerprint)))
|
|
42
|
+
.digest('hex');
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
skill_name: 'user-taste',
|
|
46
|
+
skill_version: `v1:${sourceFingerprint.slice(0, 24)}`,
|
|
47
|
+
generated_at: generatedAt,
|
|
48
|
+
generated_from: 'effective_preferences',
|
|
49
|
+
source_count: bounded.length,
|
|
50
|
+
total_source_count: active.length,
|
|
51
|
+
pending_source_count: normalized.length - active.length,
|
|
52
|
+
truncated: active.length > bounded.length,
|
|
53
|
+
scope: {
|
|
54
|
+
personal_space_id: personalSpaceId,
|
|
55
|
+
personal_project_id: personalProjectId
|
|
56
|
+
},
|
|
57
|
+
recommendations,
|
|
58
|
+
markdown: renderUserTasteSkill({
|
|
59
|
+
preferences: bounded,
|
|
60
|
+
recommendations,
|
|
61
|
+
skillVersion: `v1:${sourceFingerprint.slice(0, 24)}`,
|
|
62
|
+
generatedAt
|
|
63
|
+
})
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function recommendUserTaste(
|
|
68
|
+
preferences,
|
|
69
|
+
taskPrompt = null,
|
|
70
|
+
limit = DEFAULT_RECOMMENDATION_LIMIT
|
|
71
|
+
) {
|
|
72
|
+
const promptTokens = tokenize(taskPrompt);
|
|
73
|
+
const ranked = preferences.map((preference) => {
|
|
74
|
+
const preferenceTokens = tokenize([
|
|
75
|
+
preference.title,
|
|
76
|
+
preference.instruction,
|
|
77
|
+
preference.reason,
|
|
78
|
+
...preference.searchTerms
|
|
79
|
+
].join(' '));
|
|
80
|
+
const matchedTerms = [...promptTokens]
|
|
81
|
+
.filter((token) => preferenceTokens.has(token))
|
|
82
|
+
.sort();
|
|
83
|
+
const matchScore = promptTokens.size === 0
|
|
84
|
+
? 0
|
|
85
|
+
: Number(Math.min(1, matchedTerms.length / Math.min(promptTokens.size, 6)).toFixed(4));
|
|
86
|
+
return {
|
|
87
|
+
preference_key: preference.preferenceKey,
|
|
88
|
+
title: preference.title,
|
|
89
|
+
instruction: preference.instruction,
|
|
90
|
+
profile_aspect: preference.profileAspect,
|
|
91
|
+
preference_scope: preference.preferenceScope,
|
|
92
|
+
preference_project_id: preference.preferenceProjectId,
|
|
93
|
+
evidence_status: STATUS_LABELS[preference.confirmationStatus]
|
|
94
|
+
?? preference.confirmationStatus,
|
|
95
|
+
match_score: matchScore,
|
|
96
|
+
matched_terms: matchedTerms,
|
|
97
|
+
reason: matchedTerms.length
|
|
98
|
+
? `Matches prior preference terms: ${matchedTerms.join(', ')}`
|
|
99
|
+
: 'Available as a default profile rule; no task-specific term matched.'
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
return ranked
|
|
103
|
+
.sort((left, right) => right.match_score - left.match_score)
|
|
104
|
+
.slice(0, boundedLimit(limit, DEFAULT_RECOMMENDATION_LIMIT));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function renderUserTasteSkill({
|
|
108
|
+
preferences,
|
|
109
|
+
recommendations = [],
|
|
110
|
+
skillVersion,
|
|
111
|
+
generatedAt
|
|
112
|
+
}) {
|
|
113
|
+
const sections = [];
|
|
114
|
+
for (const [aspect, label] of Object.entries(ASPECT_LABELS)) {
|
|
115
|
+
const items = preferences.filter((preference) => preference.profileAspect === aspect);
|
|
116
|
+
if (!items.length) continue;
|
|
117
|
+
sections.push(`### ${label}`);
|
|
118
|
+
sections.push(items.map(renderPreferenceLine).join('\n'));
|
|
119
|
+
}
|
|
120
|
+
if (!sections.length) {
|
|
121
|
+
sections.push('No effective personal taste preferences are available yet.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const recommendationLines = recommendations.length
|
|
125
|
+
? recommendations.slice(0, 8).map((item) => (
|
|
126
|
+
`- **${item.title}** — ${item.instruction} ` +
|
|
127
|
+
`(${item.evidence_status}; ${item.reason})`
|
|
128
|
+
)).join('\n')
|
|
129
|
+
: '- No task-specific recommendation is available.';
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
'---',
|
|
133
|
+
'name: user-taste',
|
|
134
|
+
'description: Apply the user\'s current evidence-backed taste and working preferences when making recommendations or judgment calls.',
|
|
135
|
+
`version: ${skillVersion}`,
|
|
136
|
+
'---',
|
|
137
|
+
'',
|
|
138
|
+
'# User Taste · Generated',
|
|
139
|
+
'',
|
|
140
|
+
`Generated from FULI effective preferences at ${generatedAt}. This is a derived, read-time projection; it does not overwrite a user-authored taste Skill.`,
|
|
141
|
+
'',
|
|
142
|
+
'## Precedence',
|
|
143
|
+
'',
|
|
144
|
+
'1. The current user request and authoritative project constraints.',
|
|
145
|
+
'2. Confirmed preferences in the narrowest matching scope.',
|
|
146
|
+
'3. Observed preferences as weaker signals; never present them as user-confirmed.',
|
|
147
|
+
'4. Explain the matching rule and material tradeoff when making a recommendation.',
|
|
148
|
+
'',
|
|
149
|
+
'## Active profile rules',
|
|
150
|
+
'',
|
|
151
|
+
sections.join('\n\n'),
|
|
152
|
+
'',
|
|
153
|
+
'## Task recommendations',
|
|
154
|
+
'',
|
|
155
|
+
recommendationLines,
|
|
156
|
+
''
|
|
157
|
+
].join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizePreference(item, index) {
|
|
161
|
+
const value = item && typeof item === 'object' ? item : {};
|
|
162
|
+
const attributes = value.attributes && typeof value.attributes === 'object'
|
|
163
|
+
? value.attributes
|
|
164
|
+
: {};
|
|
165
|
+
const id = String(value.id ?? value.preference_key ?? value.key ?? `preference-${index}`);
|
|
166
|
+
const searchTerms = Array.isArray(attributes.searchTerms)
|
|
167
|
+
? attributes.searchTerms.filter((term) => typeof term === 'string' && term.trim())
|
|
168
|
+
: [];
|
|
169
|
+
return {
|
|
170
|
+
id,
|
|
171
|
+
preferenceKey: String(
|
|
172
|
+
value.preference_key ?? value.preferenceKey ?? value.key ?? id
|
|
173
|
+
),
|
|
174
|
+
title: singleLine(value.title ?? value.preference_key ?? value.key ?? id),
|
|
175
|
+
instruction: singleLine(value.instruction ?? value.summary ?? ''),
|
|
176
|
+
reason: singleLine(value.reason ?? value.reasoning_summary ?? ''),
|
|
177
|
+
profileAspect: value.profile_aspect ?? value.profileAspect ?? 'taste',
|
|
178
|
+
preferenceScope: value.preference_scope ?? value.preferenceScope ?? 'global',
|
|
179
|
+
preferenceProjectId: value.preference_project_id
|
|
180
|
+
?? value.preferenceProjectId
|
|
181
|
+
?? null,
|
|
182
|
+
confirmationStatus: value.confirmation_status
|
|
183
|
+
?? value.confirmationStatus
|
|
184
|
+
?? 'confirmed',
|
|
185
|
+
searchTerms
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function preferenceFingerprint(preference) {
|
|
190
|
+
return {
|
|
191
|
+
preferenceKey: preference.preferenceKey,
|
|
192
|
+
title: preference.title,
|
|
193
|
+
instruction: preference.instruction,
|
|
194
|
+
reason: preference.reason,
|
|
195
|
+
profileAspect: preference.profileAspect,
|
|
196
|
+
preferenceScope: preference.preferenceScope,
|
|
197
|
+
preferenceProjectId: preference.preferenceProjectId,
|
|
198
|
+
confirmationStatus: preference.confirmationStatus,
|
|
199
|
+
searchTerms: [...preference.searchTerms].sort()
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderPreferenceLine(preference) {
|
|
204
|
+
const status = STATUS_LABELS[preference.confirmationStatus]
|
|
205
|
+
?? preference.confirmationStatus;
|
|
206
|
+
const scope = preference.preferenceScope === 'project'
|
|
207
|
+
? `project:${preference.preferenceProjectId ?? 'active'}`
|
|
208
|
+
: 'global';
|
|
209
|
+
return `- **${status} · ${scope} · ${preference.preferenceKey}** ${preference.instruction}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function tokenize(value) {
|
|
213
|
+
const normalized = String(value ?? '').toLocaleLowerCase();
|
|
214
|
+
const tokens = new Set(normalized.match(/[a-z0-9][a-z0-9_-]*/g) ?? []);
|
|
215
|
+
const cjkRuns = normalized.match(/[\u3400-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g) ?? [];
|
|
216
|
+
for (const run of cjkRuns) {
|
|
217
|
+
if (run.length >= 2) tokens.add(run);
|
|
218
|
+
for (let index = 0; index < run.length - 1; index += 1) {
|
|
219
|
+
tokens.add(run.slice(index, index + 2));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return tokens;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function canonicalJson(value) {
|
|
226
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
227
|
+
if (value && typeof value === 'object') {
|
|
228
|
+
return `{${Object.keys(value).sort().map((key) =>
|
|
229
|
+
`${JSON.stringify(key)}:${canonicalJson(value[key])}`
|
|
230
|
+
).join(',')}}`;
|
|
231
|
+
}
|
|
232
|
+
return JSON.stringify(value);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function singleLine(value) {
|
|
236
|
+
return String(value ?? '').replace(/[\r\n\t]+/g, ' ').trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function boundedLimit(value, fallback) {
|
|
240
|
+
return Number.isInteger(value) && value > 0 ? Math.min(value, 100) : fallback;
|
|
241
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function providerWorkflowCandidateSearch(input) {
|
|
2
|
+
return {
|
|
3
|
+
personal_space_id: input.personalSpaceId,
|
|
4
|
+
personal_project_id: input.personalProjectId ?? null,
|
|
5
|
+
after_step_key: input.afterStepKey ?? null,
|
|
6
|
+
limit: input.limit ?? 20
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function workflowCandidatePage(value) {
|
|
11
|
+
return {
|
|
12
|
+
policy: workflowRecommendationPolicy(value.policy),
|
|
13
|
+
candidates: value.candidates.map(workflowCandidate)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function workflowCandidate(value) {
|
|
18
|
+
return {
|
|
19
|
+
candidateId: value.candidate_id,
|
|
20
|
+
candidateVersion: value.candidate_version,
|
|
21
|
+
evidenceRevision: value.evidence_revision,
|
|
22
|
+
decisionRevision: value.decision_revision,
|
|
23
|
+
ruleFingerprint: value.rule_fingerprint,
|
|
24
|
+
workflowKey: value.workflow_key,
|
|
25
|
+
condition: value.condition,
|
|
26
|
+
personalSpaceId: value.personal_space_id,
|
|
27
|
+
personalProjectId: value.personal_project_id ?? null,
|
|
28
|
+
sourceStepId: value.source_step_id,
|
|
29
|
+
sourceStepKey: value.source_step_key,
|
|
30
|
+
sourceStepName: value.source_step_name,
|
|
31
|
+
targetStepId: value.target_step_id,
|
|
32
|
+
targetStepKey: value.target_step_key,
|
|
33
|
+
targetStepName: value.target_step_name,
|
|
34
|
+
status: value.status,
|
|
35
|
+
occurrenceCount: value.occurrence_count,
|
|
36
|
+
distinctSessionCount: value.distinct_session_count,
|
|
37
|
+
recency: {
|
|
38
|
+
firstObservedAt: value.recency.first_observed_at,
|
|
39
|
+
lastObservedAt: value.recency.last_observed_at,
|
|
40
|
+
ageDays: value.recency.age_days,
|
|
41
|
+
score: value.recency.score
|
|
42
|
+
},
|
|
43
|
+
confirmationAuthority: value.confirmation_authority,
|
|
44
|
+
negativeEvidenceCount: value.negative_evidence_count,
|
|
45
|
+
declineCount: value.decline_count,
|
|
46
|
+
reviewedAt: value.reviewed_at ?? null,
|
|
47
|
+
reviewReason: value.review_reason ?? null,
|
|
48
|
+
recommendation: value.recommendation,
|
|
49
|
+
executionAuthorized: value.execution_authorized,
|
|
50
|
+
authorization: value.authorization
|
|
51
|
+
? workflowAuthorization(value.authorization)
|
|
52
|
+
: null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function workflowRecommendationPolicy(value) {
|
|
57
|
+
return {
|
|
58
|
+
minimumOccurrences: value.minimum_occurrences,
|
|
59
|
+
minimumDistinctSessions: value.minimum_distinct_sessions,
|
|
60
|
+
recommendationThreshold: value.recommendation_threshold,
|
|
61
|
+
weights: {
|
|
62
|
+
occurrences: value.weights.occurrences,
|
|
63
|
+
distinctSessions: value.weights.distinct_sessions,
|
|
64
|
+
recency: value.weights.recency,
|
|
65
|
+
confirmationAuthority: value.weights.confirmation_authority
|
|
66
|
+
},
|
|
67
|
+
declinePenalty: value.decline_penalty,
|
|
68
|
+
negativeEvidencePenalty: value.negative_evidence_penalty
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function workflowAuthorization(value) {
|
|
73
|
+
return {
|
|
74
|
+
authorizationId: value.authorization_id,
|
|
75
|
+
candidateId: value.candidate_id,
|
|
76
|
+
candidateVersion: value.candidate_version,
|
|
77
|
+
ruleId: value.rule_id,
|
|
78
|
+
ruleFingerprint: value.rule_fingerprint,
|
|
79
|
+
scope: value.scope,
|
|
80
|
+
active: value.active,
|
|
81
|
+
authority: value.authority,
|
|
82
|
+
createdAt: value.created_at,
|
|
83
|
+
highRiskPerCallApprovalRequired:
|
|
84
|
+
value.high_risk_per_call_approval_required,
|
|
85
|
+
highRiskActionCategories: value.high_risk_action_categories
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const FORBIDDEN_AGENT_FIELDS = [
|
|
4
|
+
'sessionId',
|
|
5
|
+
'idempotencyKey',
|
|
6
|
+
'observedAt',
|
|
7
|
+
'occurrenceCount',
|
|
8
|
+
'distinctSessionCount',
|
|
9
|
+
'confirmationAuthority',
|
|
10
|
+
'authority',
|
|
11
|
+
'durableAuthorizationConfirmed',
|
|
12
|
+
'approvalToken'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function providerWorkflowTransitionObservation(input) {
|
|
16
|
+
for (const field of FORBIDDEN_AGENT_FIELDS) {
|
|
17
|
+
if (Object.hasOwn(input, field)) {
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
`Workflow observations cannot submit host, aggregate, or authority field: ${field}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
assertHostValue(input.hostSessionId, 'hostSessionId');
|
|
24
|
+
assertHostValue(input.hostObservedAt, 'hostObservedAt');
|
|
25
|
+
const condition = input.condition ?? {};
|
|
26
|
+
const observationId = workflowObservationId({
|
|
27
|
+
hostSessionId: input.hostSessionId,
|
|
28
|
+
personalProjectId: input.personalProjectId ?? null,
|
|
29
|
+
workflowKey: input.workflowKey,
|
|
30
|
+
fromActionId: input.fromStep.actionId,
|
|
31
|
+
toActionId: input.toStep.actionId,
|
|
32
|
+
condition
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
personal_space_id: input.personalSpaceId,
|
|
36
|
+
personal_project_id: input.personalProjectId ?? null,
|
|
37
|
+
host_session_id: input.hostSessionId,
|
|
38
|
+
observation_id: observationId,
|
|
39
|
+
from_step: providerStep(input.fromStep),
|
|
40
|
+
to_step: providerStep(input.toStep),
|
|
41
|
+
workflow_key: input.workflowKey,
|
|
42
|
+
condition,
|
|
43
|
+
observed_at: input.hostObservedAt,
|
|
44
|
+
evidence_summary: input.evidenceSummary,
|
|
45
|
+
source_application: input.sourceApplication ?? null,
|
|
46
|
+
source_turn_id: input.sourceTurnId ?? null,
|
|
47
|
+
sensitivity: input.sensitivity ?? 'normal'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function providerStep(step) {
|
|
52
|
+
return {
|
|
53
|
+
action_id: step.actionId,
|
|
54
|
+
name: step.name,
|
|
55
|
+
summary: step.summary ?? null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function workflowObservationId(value) {
|
|
60
|
+
const digest = createHash('sha256')
|
|
61
|
+
.update(canonicalJson(value))
|
|
62
|
+
.digest('hex');
|
|
63
|
+
return `workflow-observation:${digest}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function canonicalJson(value) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
69
|
+
}
|
|
70
|
+
if (value && typeof value === 'object') {
|
|
71
|
+
return `{${Object.keys(value).sort().map((key) =>
|
|
72
|
+
`${JSON.stringify(key)}:${canonicalJson(value[key])}`
|
|
73
|
+
).join(',')}}`;
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function assertHostValue(value, label) {
|
|
79
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
80
|
+
throw new TypeError(`${label} must be injected by the MCP host`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { buildWritingTasteProfile } from './writing-taste-profile.js';
|
|
2
|
+
|
|
3
|
+
export async function getWritingTasteProfile(application, {
|
|
4
|
+
personalSpaceId,
|
|
5
|
+
personalProjectId = null,
|
|
6
|
+
limit = 500
|
|
7
|
+
} = {}) {
|
|
8
|
+
const [graph, conflictRecords] = await Promise.all([
|
|
9
|
+
application.getKnowledgeGraph({
|
|
10
|
+
spaceId: personalSpaceId,
|
|
11
|
+
limit
|
|
12
|
+
}),
|
|
13
|
+
application.personal.listPreferenceConflicts(
|
|
14
|
+
personalSpaceId,
|
|
15
|
+
null,
|
|
16
|
+
limit
|
|
17
|
+
)
|
|
18
|
+
]);
|
|
19
|
+
return buildWritingTasteProfile({
|
|
20
|
+
graph,
|
|
21
|
+
conflictRecords,
|
|
22
|
+
personalSpaceId,
|
|
23
|
+
personalProjectId
|
|
24
|
+
});
|
|
25
|
+
}
|