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.
Files changed (107) hide show
  1. package/README.md +170 -18
  2. package/README.zh-CN.md +146 -12
  3. package/compose.graphiti.yml +2 -0
  4. package/dist/web/assets/{AboutPage-qeO_vm0T.js → AboutPage-CftRexSY.js} +1 -1
  5. package/dist/web/assets/{ConnectionsPage-DO0GR9Xg.js → ConnectionsPage-D5J91x6d.js} +1 -1
  6. package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
  7. package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
  8. package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
  9. package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
  10. package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
  11. package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
  12. package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
  13. package/dist/web/assets/{OverviewPage-CmCcDCAs.js → OverviewPage-CHE6XhdS.js} +1 -1
  14. package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
  15. package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
  16. package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
  17. package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
  18. package/dist/web/assets/{ReviewPage-B3vnZ6gn.js → ReviewPage-DUeyu7E8.js} +1 -1
  19. package/dist/web/assets/{SearchableSelect-ih69qbcS.js → SearchableSelect-C5RiB5F8.js} +1 -1
  20. package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
  21. package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
  22. package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
  23. package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
  24. package/dist/web/assets/{console-DE40hXGC.js → console-DMvHxKf9.js} +1 -1
  25. package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
  26. package/dist/web/assets/{i18n-CDxBs1sz.js → i18n-CWfd8Dtt.js} +1 -1
  27. package/dist/web/assets/{identity-BCxb1mcy.js → identity-C9mLdR8L.js} +1 -1
  28. package/dist/web/assets/index-F5e2sQ7p.js +2 -0
  29. package/dist/web/assets/{index-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
  30. package/dist/web/assets/{model-CCMdt10Q.js → model-CsYAR8O3.js} +1 -1
  31. package/dist/web/assets/paths-CDREouhi.js +1 -0
  32. package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
  33. package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
  34. package/dist/web/index.html +7 -6
  35. package/graph-provider/fuli_graph/app.py +238 -2
  36. package/graph-provider/fuli_graph/auth.py +18 -0
  37. package/graph-provider/fuli_graph/collaboration_context.py +253 -17
  38. package/graph-provider/fuli_graph/common_knowledge.py +6 -2
  39. package/graph-provider/fuli_graph/config.py +5 -0
  40. package/graph-provider/fuli_graph/graph_models.py +6 -0
  41. package/graph-provider/fuli_graph/graph_projection.py +14 -0
  42. package/graph-provider/fuli_graph/graph_query.py +27 -3
  43. package/graph-provider/fuli_graph/knowledge_management.py +19 -1
  44. package/graph-provider/fuli_graph/knowledge_search.py +11 -0
  45. package/graph-provider/fuli_graph/models.py +15 -2
  46. package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
  47. package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
  48. package/graph-provider/fuli_graph/project_action_models.py +22 -0
  49. package/graph-provider/fuli_graph/project_knowledge.py +121 -2
  50. package/graph-provider/fuli_graph/provider_values.py +24 -0
  51. package/graph-provider/fuli_graph/runtime.py +78 -0
  52. package/graph-provider/fuli_graph/search_projection.py +14 -1
  53. package/graph-provider/fuli_graph/store_knowledge.py +116 -2
  54. package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
  55. package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
  56. package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
  57. package/graph-provider/fuli_graph/workflow_observations.py +84 -0
  58. package/graph-provider/pyproject.toml +1 -1
  59. package/npm-shrinkwrap.json +18 -18
  60. package/package.json +4 -4
  61. package/skills/capturing-session-knowledge/SKILL.md +32 -5
  62. package/skills/flreview/SKILL.md +4 -3
  63. package/src/agent-tools/graph-definitions.js +136 -20
  64. package/src/agent-tools/graph-handlers.js +10 -4
  65. package/src/cli/command-registry.js +1 -1
  66. package/src/cli/local-runtime-command.js +69 -2
  67. package/src/graphiti/agent-knowledge-workflows.js +497 -16
  68. package/src/graphiti/collaboration-preference-projection.js +162 -0
  69. package/src/graphiti/federated-application.js +155 -161
  70. package/src/graphiti/federated-search-support.js +102 -0
  71. package/src/graphiti/knowledge-provider-mapping.js +15 -0
  72. package/src/graphiti/provider-client.js +162 -10
  73. package/src/graphiti/related-project-suggestions.js +109 -0
  74. package/src/graphiti/runtime-config.js +14 -0
  75. package/src/graphiti/source-marker.js +2 -2
  76. package/src/graphiti/user-taste-skill.js +241 -0
  77. package/src/graphiti/workflow-candidate-mapping.js +87 -0
  78. package/src/graphiti/workflow-observation.js +82 -0
  79. package/src/graphiti/writing-taste-profile-workflow.js +25 -0
  80. package/src/graphiti/writing-taste-profile.js +490 -0
  81. package/src/http/graph-api-router.js +15 -2
  82. package/src/mcp/context-resources.js +158 -0
  83. package/src/mcp/create-mcp-server.js +39 -5
  84. package/src/mcp/instructions.js +5 -7
  85. package/src/mcp/session-id.js +60 -0
  86. package/src/mcp/task-context-registry.js +15 -3
  87. package/src/mcp/tool-annotations.js +8 -1
  88. package/src/mcp/tool-result.js +28 -4
  89. package/src/server.js +0 -1
  90. package/src/setup/claude-code-config.js +17 -6
  91. package/src/setup/graph-runtime.js +20 -2
  92. package/src/system/runtime-settings.js +121 -2
  93. package/src/system/system-service.js +5 -1
  94. package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +0 -1
  95. package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +0 -1
  96. package/dist/web/assets/KnowledgePage-BAT4aZIm.js +0 -1
  97. package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +0 -4
  98. package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +0 -1
  99. package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
  100. package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +0 -1
  101. package/dist/web/assets/PublicProjectsPage-egKBulOq.js +0 -1
  102. package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
  103. package/dist/web/assets/SettingsPage-oLE8mNs5.js +0 -1
  104. package/dist/web/assets/index-BzhNfzU_.js +0 -2
  105. package/dist/web/assets/paths-DwHeiGMT.js +0 -1
  106. package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +0 -1
  107. package/dist/web/assets/vue-router-Drfl1F8i.js +0 -1
@@ -0,0 +1,162 @@
1
+ export function agentCollaborationPreference(item) {
2
+ const preference = {
3
+ instruction: item.instruction ?? '',
4
+ preference_key: item.preference_key ?? item.key ?? item.id,
5
+ title: item.title ?? item.preference_key ?? item.key ?? item.id,
6
+ profile_aspect: item.profile_aspect ?? null,
7
+ preference_scope: item.preference_scope ?? 'global',
8
+ confirmation_status: item.confirmation_status ?? 'confirmed'
9
+ };
10
+ if (item.preference_project_id) {
11
+ preference.preference_project_id = item.preference_project_id;
12
+ }
13
+ for (const field of [
14
+ 'attributes',
15
+ 'weight',
16
+ 'reason',
17
+ 'confirmation_basis',
18
+ 'reasoning_summary',
19
+ 'inheritance_mode',
20
+ 'inherited_project_ids',
21
+ 'inherited_from_project_id',
22
+ 'scope_distance',
23
+ 'scope_path'
24
+ ]) {
25
+ if (item[field] !== undefined) preference[field] = item[field];
26
+ }
27
+ return preference;
28
+ }
29
+
30
+ export function deferredPreferenceConflicts(result, queuedConflicts) {
31
+ const items = [
32
+ ...(result.global_preferences ?? []),
33
+ ...(result.project_preferences ?? [])
34
+ ];
35
+ const itemById = new Map(items.map((item) => [item.id, item]));
36
+ const activeConflictPairs = new Set(
37
+ (result.conflicts ?? []).flatMap((conflict) => {
38
+ const ids = conflict.item_ids ?? [];
39
+ const pairs = [];
40
+ for (let left = 0; left < ids.length; left += 1) {
41
+ for (let right = left + 1; right < ids.length; right += 1) {
42
+ pairs.push(preferenceConflictPairKey(ids[left], ids[right]));
43
+ }
44
+ }
45
+ return pairs;
46
+ })
47
+ );
48
+ const queued = (queuedConflicts ?? [])
49
+ .filter((conflict) =>
50
+ conflict.status === 'ai_pending' &&
51
+ activeConflictPairs.has(preferenceConflictPairKey(
52
+ conflict.left_item_id,
53
+ conflict.right_item_id
54
+ )) &&
55
+ itemById.has(conflict.left_item_id) &&
56
+ itemById.has(conflict.right_item_id)
57
+ )
58
+ .map((conflict) => ({
59
+ ...conflict,
60
+ left: itemById.get(conflict.left_item_id),
61
+ right: itemById.get(conflict.right_item_id)
62
+ }));
63
+ const queuedPairs = new Set(queued.map((conflict) => preferenceConflictPairKey(
64
+ conflict.left_item_id,
65
+ conflict.right_item_id
66
+ )));
67
+ const inheritedScopeConflicts = (result.conflicts ?? []).flatMap((conflict) => {
68
+ const alternatives = (conflict.item_ids ?? [])
69
+ .map((id) => itemById.get(id))
70
+ .filter(Boolean);
71
+ const projectIds = [...new Set(alternatives
72
+ .map(({ inherited_from_project_id: projectId }) => projectId)
73
+ .filter(Boolean))]
74
+ .sort();
75
+ const pairAlreadyQueued = alternatives.length === 2 && queuedPairs.has(
76
+ preferenceConflictPairKey(alternatives[0].id, alternatives[1].id)
77
+ );
78
+ if (
79
+ pairAlreadyQueued ||
80
+ alternatives.length < 2 ||
81
+ projectIds.length < 2 ||
82
+ alternatives.some(({ inherited_from_project_id: projectId }) => !projectId)
83
+ ) {
84
+ return [];
85
+ }
86
+ return [{
87
+ id: `inherited-scope:${conflict.preference_key}:` +
88
+ alternatives.map(({ id }) => id).sort().join(','),
89
+ preference_key: conflict.preference_key,
90
+ preference_scope: 'project',
91
+ preference_project_ids: projectIds,
92
+ status: 'human_scope_judgment_required',
93
+ reason: 'Equally near parent projects define conflicting preferences for the active child.',
94
+ inherited_scope_conflict: true,
95
+ alternatives
96
+ }];
97
+ });
98
+ return [...queued, ...inheritedScopeConflicts];
99
+ }
100
+
101
+ export function agentDeferredPreferenceConflict(conflict) {
102
+ if (conflict.inherited_scope_conflict) {
103
+ return {
104
+ id: conflict.id,
105
+ preference_key: conflict.preference_key,
106
+ preference_scope: conflict.preference_scope,
107
+ preference_project_ids: conflict.preference_project_ids,
108
+ status: conflict.status,
109
+ reason: conflict.reason,
110
+ alternatives: conflict.alternatives.map(agentConflictPreference),
111
+ automatic_resolution: false,
112
+ requires_human_scope_judgment: true,
113
+ required_action: 'Ask a human to choose or narrow the applicable project scope; do not use weight to select a winner.',
114
+ resolution_options: []
115
+ };
116
+ }
117
+ return {
118
+ id: conflict.id,
119
+ preference_key: conflict.preference_key,
120
+ preference_scope: conflict.preference_scope,
121
+ ...(conflict.preference_project_id
122
+ ? { preference_project_id: conflict.preference_project_id }
123
+ : {}),
124
+ status: conflict.status,
125
+ deferred_at: conflict.deferred_at,
126
+ reason: conflict.reason,
127
+ left: agentConflictPreference(conflict.left),
128
+ right: agentConflictPreference(conflict.right),
129
+ required_action: 'Resolve this conflict before using either side when it is relevant to the current task.',
130
+ resolution_options: ['merge', 'keep_left', 'keep_right', 'split_scope']
131
+ };
132
+ }
133
+
134
+ function agentConflictPreference(item) {
135
+ const preference = {
136
+ item_id: item.id,
137
+ item_kind: item.item_kind,
138
+ title: item.title,
139
+ instruction: item.instruction,
140
+ confirmed_at: item.confirmed_at,
141
+ attributes: item.attributes ?? {}
142
+ };
143
+ for (const field of [
144
+ 'preference_project_id',
145
+ 'inherited_from_project_id',
146
+ 'scope_distance',
147
+ 'scope_path',
148
+ 'weight',
149
+ 'reason',
150
+ 'confirmation_basis',
151
+ 'reasoning_summary',
152
+ 'inheritance_mode',
153
+ 'inherited_project_ids'
154
+ ]) {
155
+ if (item[field] !== undefined) preference[field] = item[field];
156
+ }
157
+ return preference;
158
+ }
159
+
160
+ function preferenceConflictPairKey(leftId, rightId) {
161
+ return [leftId, rightId].sort().join('\u0000');
162
+ }
@@ -5,7 +5,7 @@ import {
5
5
  canonicalProviderUrl,
6
6
  readGraphRuntimeConfig
7
7
  } from './runtime-config.js';
8
- import { ApplicationError } from '../app/application-error.js';
8
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
9
9
  import { detectSensitiveContent } from '../security/sensitive-content.js';
10
10
  import {
11
11
  CapturePolicyStore,
@@ -38,10 +38,16 @@ import {
38
38
  beginTaskContext as beginTaskContextWorkflow,
39
39
  checkpointTaskKnowledge as checkpointTaskKnowledgeWorkflow,
40
40
  discoverCommonKnowledgeCandidates as discoverCommonKnowledgeCandidatesWorkflow,
41
+ discoverPersonalGlobalPreferenceCandidates as
42
+ discoverPersonalGlobalPreferenceCandidatesWorkflow,
43
+ applyPersonalGlobalPreferenceDecision as
44
+ applyPersonalGlobalPreferenceDecisionWorkflow,
41
45
  providerCommonKnowledgePromotion,
42
46
  recordDecisionTrace as recordDecisionTraceWorkflow,
43
47
  recordKnowledgeFeedback as recordKnowledgeFeedbackWorkflow,
44
- searchCurrentProjectKnowledge as searchCurrentProjectKnowledgeWorkflow
48
+ searchCurrentProjectKnowledge as searchCurrentProjectKnowledgeWorkflow,
49
+ previewPersonalGlobalPreferenceDecision as
50
+ previewPersonalGlobalPreferenceDecisionWorkflow
45
51
  } from './agent-knowledge-workflows.js';
46
52
  import {
47
53
  assertPublicKnowledgeEligible,
@@ -56,6 +62,29 @@ import {
56
62
  recordKnowledgeReviewProgress as recordKnowledgeReviewProgressWorkflow,
57
63
  startKnowledgeReview as startKnowledgeReviewWorkflow
58
64
  } from './knowledge-review.js';
65
+ import {
66
+ providerWorkflowCandidateSearch,
67
+ workflowCandidatePage
68
+ } from './workflow-candidate-mapping.js';
69
+ import {
70
+ providerWorkflowTransitionObservation
71
+ } from './workflow-observation.js';
72
+ import {
73
+ relatedProjectGuidance
74
+ } from './related-project-suggestions.js';
75
+ import {
76
+ agentCollaborationPreference,
77
+ agentDeferredPreferenceConflict,
78
+ deferredPreferenceConflicts
79
+ } from './collaboration-preference-projection.js';
80
+ import { buildUserTasteSkill } from './user-taste-skill.js';
81
+ import { getWritingTasteProfile as getWritingTasteProfileWorkflow } from './writing-taste-profile-workflow.js';
82
+ import {
83
+ groupSubscriptions,
84
+ loadSearchRelatedProjectSuggestions,
85
+ rankedSearchItems,
86
+ retrievalGuidanceForScope
87
+ } from './federated-search-support.js';
59
88
 
60
89
  export function openFederatedGraphApplication({
61
90
  runtimeConfigPath,
@@ -75,7 +104,8 @@ export function openFederatedGraphApplication({
75
104
  agentAccessPolicyStore: agentAccessPolicyStore ?? new AgentAccessPolicyStore(
76
105
  agentAccessPolicyPathForRuntime(runtimeConfigPath)
77
106
  ),
78
- consoleUrl: sourceConsoleUrl(runtimeConfigPath)
107
+ consoleUrl: sourceConsoleUrl(runtimeConfigPath),
108
+ providerRequestTimeoutMs: providerRequestTimeoutFromEnv(env)
79
109
  });
80
110
  if (typeof runtimeConfigPath === 'string' && runtimeConfigPath) {
81
111
  attachExternalKnowledgeRuntime(app, {
@@ -87,6 +117,18 @@ export function openFederatedGraphApplication({
87
117
  return app;
88
118
  }
89
119
 
120
+ function providerRequestTimeoutFromEnv(env) {
121
+ const value = env?.FULI_PROVIDER_REQUEST_TIMEOUT_MS;
122
+ if (value === undefined || value === null || value === '') return undefined;
123
+ const parsed = Number(value);
124
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 2_147_483_647) {
125
+ throw new TypeError(
126
+ 'FULI_PROVIDER_REQUEST_TIMEOUT_MS must be a positive safe integer'
127
+ );
128
+ }
129
+ return parsed;
130
+ }
131
+
90
132
  export class FederatedGraphApplication {
91
133
  constructor(config, {
92
134
  fetchImpl = globalThis.fetch,
@@ -107,6 +149,7 @@ export class FederatedGraphApplication {
107
149
  this.personal = new GraphitiProviderClient({
108
150
  baseUrl: config.personal.providerUrl,
109
151
  accessToken: config.personal.accessToken,
152
+ workflowObservationToken: config.personal.workflowObservationToken ?? null,
110
153
  fetchImpl,
111
154
  requestTimeoutMs: providerRequestTimeoutMs
112
155
  });
@@ -156,7 +199,10 @@ export class FederatedGraphApplication {
156
199
  const episode = providerEpisode(input);
157
200
  if (input.targetKind === 'personal') {
158
201
  if (input.spaceId !== this.config.personal.spaceId) {
159
- throw new TypeError('Personal capture target must be the active personal space');
202
+ throw new ApplicationError(
203
+ ApplicationErrorCode.VALIDATION,
204
+ 'Personal capture target must be the active personal space'
205
+ );
160
206
  }
161
207
  const result = await this.personal.commit({
162
208
  space_id: input.spaceId,
@@ -166,7 +212,10 @@ export class FederatedGraphApplication {
166
212
  return { route: 'personal', ...result };
167
213
  }
168
214
  if (episode.sensitivity !== 'normal') {
169
- throw new TypeError('Private or restricted knowledge cannot enter a team-shared project queue');
215
+ throw new ApplicationError(
216
+ ApplicationErrorCode.VALIDATION,
217
+ 'Private or restricted knowledge cannot enter a team-shared project queue. Use targetKind "personal" with personalProjectId to keep it in the personal graph'
218
+ );
170
219
  }
171
220
  assertPublicKnowledgeEligible(episode);
172
221
  const workspace = this.#workspace(input.providerUrl);
@@ -185,6 +234,23 @@ export class FederatedGraphApplication {
185
234
  };
186
235
  }
187
236
 
237
+ async recordWorkflowTransitionObservation(input) {
238
+ this.#assertActivePersonalSpace(input.personalSpaceId);
239
+ const capturePolicy = this.getCapturePolicy();
240
+ if (!capturePolicy.enabled) {
241
+ return {
242
+ route: 'disabled',
243
+ status: 'capture_disabled',
244
+ capturePolicy
245
+ };
246
+ }
247
+ assertNoCredentials(input);
248
+ const result = await this.personal.recordWorkflowObservation(
249
+ providerWorkflowTransitionObservation(input)
250
+ );
251
+ return { route: 'personal', ...result };
252
+ }
253
+
188
254
  async getCollaborationPreferences({
189
255
  personalProjectId = null,
190
256
  projectPath = null,
@@ -231,8 +297,10 @@ export class FederatedGraphApplication {
231
297
  apply: 'effective_preferences',
232
298
  global_scope: 'Apply personal-global preferences in every user task.',
233
299
  project_scope: resolvedProjectId
234
- ? `Also apply preferences scoped to the matched personal project ${resolvedProjectId}.`
300
+ ? `Also apply preferences scoped to ${resolvedProjectId} and confirmed ancestor preferences whose descendants or selected-projects mode explicitly reaches it.`
235
301
  : 'No exact personal project matched; do not apply project-scoped preferences.',
302
+ inheritance: 'Project preference inheritance follows only PART_OF or USES_KNOWLEDGE_FROM for at most two hops. Preserve inherited_from_project_id, scope_path, and scope_distance. An exact item wins only after confirmation authority is compared; weight never expands scope or resolves a conflict.',
303
+ related_projects: 'RELATED_TO preferences never auto-apply. Use structured related-project suggestions to ask before a one-time read-only expansion.',
236
304
  conflicts: 'Do not apply entries listed in conflicts until the user resolves them.',
237
305
  deferred_conflicts: 'If the current task would use a deferred_conflict, call resolve_deferred_preference_conflict before applying either side. Ignore unrelated deferred conflicts. The resolution must preserve the AI audit marker.',
238
306
  authority: 'Human or authoritative-source confirmed preferences outrank agent-confirmed preferences. Agent-confirmed preferences are usable but lower priority and remain explicitly marked.',
@@ -259,6 +327,9 @@ export class FederatedGraphApplication {
259
327
  personal_project_id: result.personal_project_id,
260
328
  global_preference_count: (result.global_preferences ?? []).length,
261
329
  project_preference_count: (result.project_preferences ?? []).length,
330
+ inherited_project_preference_count: (result.project_preferences ?? [])
331
+ .filter(({ inherited_from_project_id: projectId }) => Boolean(projectId))
332
+ .length,
262
333
  conflict_count: (result.conflicts ?? []).length,
263
334
  ai_deferred_conflict_count: deferredConflicts.length,
264
335
  overridden_global_count: (result.overridden_global_ids ?? []).length,
@@ -268,6 +339,43 @@ export class FederatedGraphApplication {
268
339
  };
269
340
  }
270
341
 
342
+ async getUserTasteSkill({
343
+ personalProjectId = null,
344
+ projectPath = null,
345
+ taskPrompt = null,
346
+ limit = 100
347
+ } = {}) {
348
+ const context = await this.getCollaborationPreferences({
349
+ personalProjectId,
350
+ projectPath,
351
+ taskPrompt,
352
+ limit,
353
+ agentInvocation: true,
354
+ agentToolName: 'get_user_taste_skill'
355
+ });
356
+ const skill = buildUserTasteSkill({
357
+ preferences: context.effective_preferences,
358
+ taskPrompt,
359
+ personalSpaceId: context.context.personal_space_id,
360
+ personalProjectId: context.context.personal_project_id
361
+ });
362
+ return {
363
+ status: 'generated',
364
+ ...skill,
365
+ ...(context.task_knowledge_recall
366
+ ? { task_knowledge_recall: context.task_knowledge_recall }
367
+ : {}),
368
+ application_guidance: context.application_guidance,
369
+ deferred_conflicts: context.deferred_conflicts,
370
+ context: context.context
371
+ };
372
+ }
373
+
374
+ async getWritingTasteProfile(input = {}) {
375
+ this.#assertActivePersonalSpace(input.personalSpaceId);
376
+ return getWritingTasteProfileWorkflow(this, input);
377
+ }
378
+
271
379
  async #resolvePreferenceProject({ personalProjectId, projectPath }) {
272
380
  if (projectPath !== undefined && projectPath !== null) {
273
381
  const projects = await this.personal.listPersonalProjects(
@@ -432,6 +540,10 @@ export class FederatedGraphApplication {
432
540
  .map(({ id }) => ({ item_id: id, item_kind: 'entity' }))
433
541
  ], agentToolName);
434
542
  }
543
+ const relatedProjects = await loadSearchRelatedProjectSuggestions(
544
+ this,
545
+ { facts, entities }
546
+ );
435
547
  return {
436
548
  query,
437
549
  sourceMarker,
@@ -446,7 +558,10 @@ export class FederatedGraphApplication {
446
558
  searchedProjectIds,
447
559
  failedProjectIds: unavailableProjectIds,
448
560
  partial: unavailableProjectIds.length > 0,
449
- subscriptions: selectedSubscriptions
561
+ subscriptions: selectedSubscriptions,
562
+ relatedProjectSuggestions: relatedProjects.suggestions,
563
+ relatedProjectSuggestionsStatus: relatedProjects.status,
564
+ relatedProjectGuidance: relatedProjectGuidance(relatedProjects.suggestions)
450
565
  };
451
566
  }
452
567
 
@@ -463,6 +578,23 @@ export class FederatedGraphApplication {
463
578
  return discoverCommonKnowledgeCandidatesWorkflow(this, input);
464
579
  }
465
580
 
581
+ async discoverPersonalGlobalPreferenceCandidates(input) {
582
+ this.#assertActivePersonalSpace(input.personalSpaceId);
583
+ return discoverPersonalGlobalPreferenceCandidatesWorkflow(this, input);
584
+ }
585
+
586
+ async previewPersonalGlobalPreferenceDecision(input) {
587
+ this.#assertActivePersonalSpace(input.personalSpaceId);
588
+ assertSafeKnowledgeMutation(input);
589
+ return previewPersonalGlobalPreferenceDecisionWorkflow(this, input);
590
+ }
591
+
592
+ async applyPersonalGlobalPreferenceDecision(input) {
593
+ this.#assertActivePersonalSpace(input.personalSpaceId);
594
+ assertSafeKnowledgeMutation(input);
595
+ return applyPersonalGlobalPreferenceDecisionWorkflow(this, input);
596
+ }
597
+
466
598
  async previewCommonKnowledgePromotion(input) {
467
599
  this.#assertActivePersonalSpace(input.personalSpaceId);
468
600
  assertSafeKnowledgeMutation(input);
@@ -531,6 +663,22 @@ export class FederatedGraphApplication {
531
663
  return finishKnowledgeReviewWorkflow(this, input);
532
664
  }
533
665
 
666
+ async listWorkflowCandidates(input) {
667
+ this.#assertActivePersonalSpace(input.personalSpaceId);
668
+ const result = await this.personal.searchWorkflowCandidates(
669
+ providerWorkflowCandidateSearch(input)
670
+ );
671
+ return workflowCandidatePage(result);
672
+ }
673
+
674
+ async recommendNextWorkflowSteps(input) {
675
+ this.#assertActivePersonalSpace(input.personalSpaceId);
676
+ const result = await this.personal.recommendWorkflowCandidates(
677
+ providerWorkflowCandidateSearch(input)
678
+ );
679
+ return workflowCandidatePage(result);
680
+ }
681
+
534
682
  async recordKnowledgeUsage({
535
683
  personalSpaceId,
536
684
  taskId,
@@ -1095,89 +1243,6 @@ export class FederatedGraphApplication {
1095
1243
  }
1096
1244
  }
1097
1245
 
1098
- function agentCollaborationPreference(item) {
1099
- const preference = {
1100
- instruction: item.instruction ?? '',
1101
- preference_key: item.preference_key ?? item.key ?? item.id,
1102
- title: item.title ?? item.preference_key ?? item.key ?? item.id,
1103
- profile_aspect: item.profile_aspect ?? null,
1104
- preference_scope: item.preference_scope ?? 'global',
1105
- confirmation_status: item.confirmation_status ?? 'confirmed'
1106
- };
1107
- if (item.preference_project_id) {
1108
- preference.preference_project_id = item.preference_project_id;
1109
- }
1110
- return preference;
1111
- }
1112
-
1113
- function deferredPreferenceConflicts(result, queuedConflicts) {
1114
- const items = [
1115
- ...(result.global_preferences ?? []),
1116
- ...(result.project_preferences ?? [])
1117
- ];
1118
- const itemById = new Map(items.map((item) => [item.id, item]));
1119
- const activeConflictPairs = new Set(
1120
- (result.conflicts ?? []).flatMap((conflict) => {
1121
- const ids = conflict.item_ids ?? [];
1122
- const pairs = [];
1123
- for (let left = 0; left < ids.length; left += 1) {
1124
- for (let right = left + 1; right < ids.length; right += 1) {
1125
- pairs.push(preferenceConflictPairKey(ids[left], ids[right]));
1126
- }
1127
- }
1128
- return pairs;
1129
- })
1130
- );
1131
- return (queuedConflicts ?? [])
1132
- .filter((conflict) =>
1133
- conflict.status === 'ai_pending' &&
1134
- activeConflictPairs.has(preferenceConflictPairKey(
1135
- conflict.left_item_id,
1136
- conflict.right_item_id
1137
- )) &&
1138
- itemById.has(conflict.left_item_id) &&
1139
- itemById.has(conflict.right_item_id)
1140
- )
1141
- .map((conflict) => ({
1142
- ...conflict,
1143
- left: itemById.get(conflict.left_item_id),
1144
- right: itemById.get(conflict.right_item_id)
1145
- }));
1146
- }
1147
-
1148
- function agentDeferredPreferenceConflict(conflict) {
1149
- return {
1150
- id: conflict.id,
1151
- preference_key: conflict.preference_key,
1152
- preference_scope: conflict.preference_scope,
1153
- ...(conflict.preference_project_id
1154
- ? { preference_project_id: conflict.preference_project_id }
1155
- : {}),
1156
- status: conflict.status,
1157
- deferred_at: conflict.deferred_at,
1158
- reason: conflict.reason,
1159
- left: agentConflictPreference(conflict.left),
1160
- right: agentConflictPreference(conflict.right),
1161
- required_action: 'Resolve this conflict before using either side when it is relevant to the current task.',
1162
- resolution_options: ['merge', 'keep_left', 'keep_right', 'split_scope']
1163
- };
1164
- }
1165
-
1166
- function agentConflictPreference(item) {
1167
- return {
1168
- item_id: item.id,
1169
- item_kind: item.item_kind,
1170
- title: item.title,
1171
- instruction: item.instruction,
1172
- confirmed_at: item.confirmed_at,
1173
- attributes: item.attributes ?? {}
1174
- };
1175
- }
1176
-
1177
- function preferenceConflictPairKey(leftId, rightId) {
1178
- return [leftId, rightId].sort().join('\u0000');
1179
- }
1180
-
1181
1246
  function assertNoCredentials(input) {
1182
1247
  const content = JSON.stringify({
1183
1248
  name: input.name,
@@ -1196,49 +1261,6 @@ function assertNoCredentials(input) {
1196
1261
  }
1197
1262
  }
1198
1263
 
1199
- function retrievalGuidanceForScope(personalProjectScope) {
1200
- if (personalProjectScope === 'all_local_confirmed') {
1201
- return {
1202
- currentPersonalProjectScope: 'all_local_confirmed',
1203
- markerToUseIfNoSupportingEvidence: 'noMatchSourceMarker',
1204
- requiredNextActionIfNoSupportingEvidence:
1205
- 'search_current_workspace_files_or_ask_for_safe_root',
1206
- workspaceFileSearch: {
1207
- available: true,
1208
- consentSource: 'bounded_expansion_confirmation',
1209
- rootBoundary: 'current_working_directory',
1210
- requiresSafeProjectOrWorkspaceRoot: true,
1211
- forbiddenBroadRoots: ['user_home', 'filesystem_root'],
1212
- readOnly: true,
1213
- includesPublicProjects: false
1214
- },
1215
- instruction: 'Use read-only local file search in the current repository or explicit ' +
1216
- 'workspace root, preserving exact names first. If the working directory is the user ' +
1217
- 'home, filesystem root, or otherwise too broad, ask for a safe root. Never search ' +
1218
- 'outside that root or inspect credential stores; if no evidence supports the answer, ' +
1219
- 'ask for a source clue.'
1220
- };
1221
- }
1222
- return {
1223
- currentPersonalProjectScope: 'bounded',
1224
- markerToUseIfNoSupportingEvidence: 'noMatchSourceMarker',
1225
- requiredNextActionIfNoSupportingEvidence:
1226
- 'ask_user_to_confirm_all_local_and_workspace_search',
1227
- expansion: {
1228
- available: true,
1229
- requiresExplicitUserConfirmation: true,
1230
- input: { personalProjectScope: 'all_local_confirmed' },
1231
- readOnly: true,
1232
- oneQueryOnly: true,
1233
- includesPublicProjects: false,
1234
- includesCurrentWorkspaceFiles: true
1235
- },
1236
- instruction: 'Ask whether to widen this one read-only lookup to all registered local ' +
1237
- 'personal projects and, if still unresolved, current repository or workspace files. ' +
1238
- 'Exclude public projects and paths outside the current workspace; then stop and wait.'
1239
- };
1240
- }
1241
-
1242
1264
  function assertSafeKnowledgeMutation(input) {
1243
1265
  const content = JSON.stringify({
1244
1266
  reason: input.reason,
@@ -1262,34 +1284,6 @@ function assertSafeKnowledgeMutation(input) {
1262
1284
  }
1263
1285
  }
1264
1286
 
1265
- function groupSubscriptions(subscriptions) {
1266
- const grouped = new Map();
1267
- for (const subscription of subscriptions) {
1268
- const url = canonicalProviderUrl(subscription.provider_url);
1269
- if (!grouped.has(url)) grouped.set(url, []);
1270
- grouped.get(url).push(subscription);
1271
- }
1272
- return grouped;
1273
- }
1274
-
1275
- function rankedSearchItems(searchResults, key, limit) {
1276
- return searchResults
1277
- .flatMap((result) => result[key])
1278
- .map((item, index) => ({ item, index }))
1279
- .sort((left, right) =>
1280
- searchItemScore(right.item) - searchItemScore(left.item) ||
1281
- left.index - right.index
1282
- )
1283
- .slice(0, limit)
1284
- .map(({ item }) => item);
1285
- }
1286
-
1287
- function searchItemScore(item) {
1288
- return typeof item?.score === 'number' && Number.isFinite(item.score)
1289
- ? item.score
1290
- : 0;
1291
- }
1292
-
1293
1287
  async function safely(operation, fallback) {
1294
1288
  try {
1295
1289
  return await operation();