fuli-context 0.7.4-beta.3 → 0.7.6

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 (130) hide show
  1. package/README.md +77 -6
  2. package/README.zh-CN.md +69 -6
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-sLV9WChi.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-BWn4ywGJ.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CRQWOaos.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-Dfj3vIpU.js +1 -0
  9. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-ChEB5Bwk.js +4 -0
  11. package/dist/web/assets/OverviewPage-Cd66M0RT.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-RCFHL0Wz.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-mstPEjHe.js +1 -0
  17. package/dist/web/assets/ReviewPage-6Y1SmfZy.js +1 -0
  18. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +1 -0
  19. package/dist/web/assets/SettingsPage--LdTMUm7.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-CIpKswvs.js +1 -0
  22. package/dist/web/assets/{console-D_2zC4lm.js → console-BT1P2Db4.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DrJEotdf.js} +1 -1
  24. package/dist/web/assets/i18n-js6gfc7j.js +9 -0
  25. package/dist/web/assets/{identity-DmGcY3Y6.js → identity-CfDfanpa.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-GqJf1xqg.js +2 -0
  28. package/dist/web/assets/{model-CVoOdRsL.js → model-DJkLAJsg.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-BdSti7Y2.js} +1 -1
  30. package/dist/web/assets/vue-router-Kk67EaCP.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/graph-provider/fuli_graph/app.py +8 -0
  33. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  34. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  35. package/graph-provider/fuli_graph/model_validation.py +130 -0
  36. package/graph-provider/fuli_graph/models.py +24 -5
  37. package/graph-provider/fuli_graph/project_agent_access.py +54 -0
  38. package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
  39. package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
  40. package/graph-provider/fuli_graph/project_agent_models.py +278 -0
  41. package/graph-provider/fuli_graph/project_agent_routes.py +626 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +545 -0
  43. package/graph-provider/fuli_graph/runtime.py +259 -0
  44. package/graph-provider/fuli_graph/search_projection.py +3 -0
  45. package/graph-provider/fuli_graph/store.py +6 -0
  46. package/graph-provider/fuli_graph/store_knowledge.py +62 -9
  47. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  50. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +705 -0
  53. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +586 -0
  54. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1151 -0
  55. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  56. package/graph-provider/pyproject.toml +2 -1
  57. package/npm-shrinkwrap.json +2 -2
  58. package/package.json +1 -1
  59. package/skills/capturing-session-knowledge/SKILL.md +39 -0
  60. package/src/adaptive-runtime/executor-pool.js +127 -0
  61. package/src/adaptive-runtime/graph-services.js +166 -0
  62. package/src/adaptive-runtime/lease-client.js +335 -0
  63. package/src/adaptive-runtime/runtime-broker.js +318 -0
  64. package/src/adaptive-runtime/settings.js +111 -0
  65. package/src/adaptive-runtime/state.js +74 -0
  66. package/src/agent-tools/graph-definitions.js +419 -0
  67. package/src/agent-tools/graph-handlers.js +65 -0
  68. package/src/agent-tools/project-agent-definitions.js +307 -0
  69. package/src/backup/graph-backup.js +289 -0
  70. package/src/backup/identity-reconciliation.js +208 -0
  71. package/src/backup/runtime-adapters.js +269 -0
  72. package/src/cli/command-registry.js +6 -2
  73. package/src/cli/graph-data-command.js +112 -0
  74. package/src/cli/local-runtime-command.js +16 -2
  75. package/src/cli/setup-command.js +19 -3
  76. package/src/cli/update-command.js +4 -0
  77. package/src/cli.js +6 -0
  78. package/src/graphiti/collaboration-preference-projection.js +29 -1
  79. package/src/graphiti/collaboration-preference-workflow.js +121 -0
  80. package/src/graphiti/federated-application.js +53 -90
  81. package/src/graphiti/project-agent-control-plane.js +275 -0
  82. package/src/graphiti/project-agent-mapping.js +1024 -0
  83. package/src/graphiti/project-agent-workflows.js +576 -0
  84. package/src/graphiti/provider-client.js +282 -1
  85. package/src/http/api-router.js +63 -9
  86. package/src/http/graph-api-router.js +290 -0
  87. package/src/local-runtime/lifecycle.js +46 -10
  88. package/src/mcp/context-resources.js +15 -6
  89. package/src/mcp/create-mcp-server.js +51 -16
  90. package/src/mcp/instructions.js +7 -5
  91. package/src/mcp/runtime.js +17 -5
  92. package/src/mcp/session-id.js +38 -1
  93. package/src/mcp/tool-annotations.js +71 -0
  94. package/src/mcp-server.js +23 -6
  95. package/src/native-runtime/runtime.js +717 -0
  96. package/src/server.js +6 -2
  97. package/src/setup/agent-installation-status.js +7 -1
  98. package/src/setup/agents.js +3 -1
  99. package/src/setup/claude-code-config.js +3 -1
  100. package/src/setup/codex-bootstrap.js +11 -10
  101. package/src/setup/codex-config.js +7 -1
  102. package/src/setup/cursor-config.js +3 -1
  103. package/src/setup/graph-runtime.js +158 -13
  104. package/src/setup/neo4j-memory-profile.js +43 -0
  105. package/src/setup/options.js +38 -2
  106. package/src/setup/paths.js +14 -0
  107. package/src/setup/setup.js +37 -1
  108. package/src/system/resource-monitor.js +181 -16
  109. package/src/system/runtime-settings.js +10 -1
  110. package/src/system/system-service.js +27 -2
  111. package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
  112. package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
  113. package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
  114. package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
  115. package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
  116. package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
  117. package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
  118. package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
  119. package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
  120. package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
  121. package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
  122. package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
  123. package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
  124. package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
  125. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  126. package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
  127. package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
  128. package/dist/web/assets/index-IUGLt4lG.js +0 -2
  129. package/dist/web/assets/index-X2j29j8i.css +0 -1
  130. package/dist/web/assets/vue-router-CWSsUUio.js +0 -1
@@ -66,6 +66,71 @@ const HANDLERS = Object.freeze({
66
66
  app.upsertPersonalProject(input)
67
67
  ),
68
68
  list_personal_projects: (app, input) => app.listPersonalProjects(input),
69
+ upsert_project_agent: (app, input) => captureGuard(app, () =>
70
+ app.upsertProjectAgent(input)
71
+ ),
72
+ list_project_agents: (app, input) => app.listCurrentProjectAgents(input),
73
+ get_project_agent: (app, input) => app.getProjectAgent(input),
74
+ delete_project_agent: (app, input) => app.deleteProjectAgent(input),
75
+ cleanup_test_project_agents: (app, input) => app.cleanupProjectAgentTestRoles(input),
76
+ create_project_agent_assignment: (app, input) =>
77
+ app.createProjectAgentAssignment(input),
78
+ list_project_agent_assignments: (app, input) =>
79
+ app.listProjectAgentAssignments(input),
80
+ end_project_agent_assignment: (app, input) =>
81
+ app.endProjectAgentAssignment(input),
82
+ replace_project_agent_assignment: (app, input) =>
83
+ app.replaceProjectAgentAssignment(input),
84
+ get_project_agent_context: (app, input) => app.getProjectAgentContext(input),
85
+ coordinate_project_agent_task: (app, input) =>
86
+ app.coordinateProjectAgentTask(input),
87
+ acquire_runtime_lease: (app, input) => app.acquireRuntimeLease(input),
88
+ refresh_runtime_lease: (app, input) => app.refreshRuntimeLease(input),
89
+ release_runtime_lease: (app, input) => app.releaseRuntimeLease(input),
90
+ submit_project_agent_task: (app, input) => app.submitProjectAgentTask(input),
91
+ view_project_agent_task: (app, input) => app.viewProjectAgentTask(input),
92
+ record_project_agent_task_activity: (app, input) =>
93
+ app.recordProjectAgentTaskActivity(input),
94
+ view_project_agent_activity: (app, input) => app.viewProjectAgentActivity(input),
95
+ get_project_agent_coordination_policy: (app, input) =>
96
+ app.getProjectAgentCoordinationPolicy(input),
97
+ update_project_agent_coordination_policy: (app, input) =>
98
+ app.updateProjectAgentCoordinationPolicy(input),
99
+ get_project_agent_recruitment_policy: (app, input) =>
100
+ app.getProjectAgentRecruitmentPolicy(input),
101
+ update_project_agent_recruitment_policy: (app, input) =>
102
+ app.updateProjectAgentRecruitmentPolicy(input),
103
+ list_project_agent_recruitments: (app, input) =>
104
+ app.listProjectAgentRecruitments(input),
105
+ decide_project_agent_recruitment: (app, input) =>
106
+ app.decideProjectAgentRecruitment(input),
107
+ upsert_executor: (app, input) => app.upsertExecutor(input),
108
+ list_executors: (app, input) => app.listExecutors(input),
109
+ get_executor: (app, input) => app.getExecutor(input),
110
+ delete_executor: (app, input) => app.deleteExecutor(input),
111
+ preflight_executor: (app, input) => app.preflightExecutor(input),
112
+ authorize_executor: (app, input) => app.authorizeExecutor(input),
113
+ report_executor_health: (app, input) => app.reportExecutorHealth(input),
114
+ record_project_agent_executor_actual: (app, input) =>
115
+ app.recordProjectAgentExecutorActual(input),
116
+ upsert_executor_routing_rule: (app, input) =>
117
+ app.upsertExecutorRoutingRule(input),
118
+ update_executor_routing_rule: (app, input) =>
119
+ app.updateExecutorRoutingRule(input),
120
+ list_executor_routing_rules: (app, input) =>
121
+ app.listExecutorRoutingRules(input),
122
+ get_executor_routing_rule: (app, input) =>
123
+ app.getExecutorRoutingRule(input),
124
+ delete_executor_routing_rule: (app, input) =>
125
+ app.deleteExecutorRoutingRule(input),
126
+ record_project_agent_task_outcome: (app, input) =>
127
+ app.recordProjectAgentTaskOutcome(input),
128
+ list_project_agent_routing_learning: (app, input) =>
129
+ app.listProjectAgentRoutingLearning(input),
130
+ ignore_project_agent_routing_learning: (app, input) =>
131
+ app.ignoreProjectAgentRoutingLearning(input),
132
+ reset_project_agent_routing_learning: (app, input) =>
133
+ app.resetProjectAgentRoutingLearning(input),
69
134
  start_knowledge_review: (app, input) => app.startKnowledgeReview(input),
70
135
  list_knowledge_review_candidates: (app, input) =>
71
136
  app.listKnowledgeReviewCandidates(input),
@@ -0,0 +1,307 @@
1
+ import {
2
+ arraySchema,
3
+ booleanSchema,
4
+ enumSchema,
5
+ integerSchema,
6
+ nullableStringSchema,
7
+ objectSchema,
8
+ stringSchema
9
+ } from './schema.js';
10
+
11
+ const id = boundedString(256);
12
+ const shortText = boundedString(2048);
13
+ const idempotencyKey = { ...boundedString(256), minLength: 8 };
14
+ const dateTime = { ...boundedString(64), format: 'date-time' };
15
+ const sourceApplication = {
16
+ type: ['string', 'null'],
17
+ enum: ['codex', 'claude_code', 'cursor', 'kiro', 'other', null]
18
+ };
19
+
20
+ const projectAgentStatus = enumSchema(['active', 'inactive', 'archived']);
21
+ export const nullableProjectAgentStatus = {
22
+ type: ['string', 'null'],
23
+ enum: ['active', 'inactive', 'archived', null]
24
+ };
25
+ const projectAgentExecutorPolicy = objectSchema({
26
+ mode: enumSchema(['flexible', 'locked']),
27
+ lockedExecutorIds: arraySchema(id, { maxItems: 32 }),
28
+ preferredExecutorIds: arraySchema(id, { maxItems: 32 })
29
+ });
30
+ export const projectAgentModelStrategy = objectSchema({
31
+ mode: enumSchema(['adaptive', 'fast', 'balanced', 'deep']),
32
+ reasoningEffort: enumSchema(['default', 'low', 'medium', 'high']),
33
+ capabilityHints: arraySchema(boundedString(128), { maxItems: 16 })
34
+ });
35
+ export const projectAgentProfile = objectSchema({
36
+ name: boundedString(160),
37
+ responsibility: boundedString(4096),
38
+ occupationEmoji: { ...nullableStringSchema(), minLength: 1, maxLength: 32 },
39
+ agentType: enumSchema(['coordinator', 'durable', 'hr', 'temporary']),
40
+ workKinds: arraySchema(boundedString(512), { maxItems: 32 }),
41
+ capabilities: arraySchema(boundedString(512), { maxItems: 32 }),
42
+ initialPreferences: arraySchema(boundedString(512), { maxItems: 32 }),
43
+ defaultModelStrategy: projectAgentModelStrategy,
44
+ executorPolicy: projectAgentExecutorPolicy,
45
+ allowedClients: arraySchema(
46
+ enumSchema(['codex', 'claude_code', 'cursor', 'kiro', 'other']),
47
+ { maxItems: 5 }
48
+ ),
49
+ testSource: nullableStringSchema(),
50
+ cleanupEligible: booleanSchema(),
51
+ status: projectAgentStatus
52
+ }, ['name', 'responsibility']);
53
+
54
+ export const projectAgentAssignmentStatus = enumSchema(['active', 'ended']);
55
+ export const projectAgentAssignmentInput = objectSchema({
56
+ personalSpaceId: id,
57
+ personalProjectId: id,
58
+ agentId: id,
59
+ idempotencyKey,
60
+ responsibility: boundedString(4096),
61
+ workKinds: arraySchema(boundedString(512), { maxItems: 32 }),
62
+ capabilities: arraySchema(boundedString(512), { maxItems: 32 }),
63
+ modelStrategyOverride: { ...projectAgentProfile.properties.defaultModelStrategy, type: ['object', 'null'] },
64
+ executorPolicyOverride: { ...projectAgentExecutorPolicy, type: ['object', 'null'] },
65
+ reason: shortText,
66
+ sourceApplication,
67
+ sourceSessionId: nullableStringSchema()
68
+ }, ['personalSpaceId', 'personalProjectId', 'agentId', 'idempotencyKey', 'responsibility', 'reason']);
69
+ export const projectAgentAssignmentEndInput = objectSchema({
70
+ personalSpaceId: id,
71
+ personalProjectId: id,
72
+ assignmentId: id,
73
+ expectedRevision: integerSchema({ minimum: 0 }),
74
+ reason: shortText
75
+ }, ['personalSpaceId', 'personalProjectId', 'assignmentId', 'expectedRevision', 'reason']);
76
+ export const projectAgentAssignmentReplaceInput = objectSchema({
77
+ personalSpaceId: id,
78
+ personalProjectId: id,
79
+ assignmentId: id,
80
+ expectedRevision: integerSchema({ minimum: 0 }),
81
+ replacementAgentId: id,
82
+ idempotencyKey,
83
+ responsibility: boundedString(4096),
84
+ workKinds: arraySchema(boundedString(512), { maxItems: 32 }),
85
+ capabilities: arraySchema(boundedString(512), { maxItems: 32 }),
86
+ modelStrategyOverride: { ...projectAgentProfile.properties.defaultModelStrategy, type: ['object', 'null'] },
87
+ executorPolicyOverride: { ...projectAgentExecutorPolicy, type: ['object', 'null'] },
88
+ reason: shortText,
89
+ sourceApplication,
90
+ sourceSessionId: nullableStringSchema()
91
+ }, [
92
+ 'personalSpaceId', 'personalProjectId', 'idempotencyKey',
93
+ 'responsibility', 'reason', 'assignmentId', 'expectedRevision', 'replacementAgentId'
94
+ ]);
95
+ const projectAgentTaskStatus = enumSchema([
96
+ 'awaiting_recruitment', 'queued', 'running', 'paused', 'failed',
97
+ 'awaiting_review', 'blocked', 'completed', 'cancelled'
98
+ ]);
99
+ const projectAgentTaskDuration = enumSchema(['ongoing', 'one_off']);
100
+ const projectAgentStaffingIntent = enumSchema([
101
+ 'reuse_preferred', 'new_durable', 'temporary', 'unassigned'
102
+ ]);
103
+ const projectAgentTaskComplexity = enumSchema(['simple', 'standard', 'complex']);
104
+ const projectAgentParallelPlan = objectSchema({
105
+ enabled: booleanSchema(),
106
+ independentVerification: booleanSchema(),
107
+ conflictFreeScopes: booleanSchema(),
108
+ reason: nullableStringSchema(),
109
+ workstreamBoundaries: arraySchema(shortText, { maxItems: 16 })
110
+ });
111
+ export const projectAgentTaskSubmitInput = objectSchema({
112
+ personalSpaceId: id,
113
+ personalProjectId: id,
114
+ idempotencyKey,
115
+ title: boundedString(160),
116
+ objective: boundedString(4096),
117
+ workKind: boundedString(128),
118
+ requiredCapabilities: arraySchema(boundedString(512), { maxItems: 16 }),
119
+ duration: projectAgentTaskDuration,
120
+ staffingIntent: projectAgentStaffingIntent,
121
+ leadAgentId: nullableStringSchema(),
122
+ collaboratorAgentIds: arraySchema(id, { maxItems: 16 }),
123
+ coordinatorAgentId: nullableStringSchema(),
124
+ complexityHint: { ...projectAgentTaskComplexity, type: ['string', 'null'] },
125
+ parallelPlan: projectAgentParallelPlan,
126
+ modelStrategyOverride: { ...projectAgentProfile.properties.defaultModelStrategy, type: ['object', 'null'] },
127
+ executorPolicyOverride: { ...projectAgentExecutorPolicy, type: ['object', 'null'] },
128
+ sourceApplication,
129
+ sourceSessionId: nullableStringSchema(),
130
+ routingReason: shortText,
131
+ recruitmentProfile: { ...projectAgentProfile, type: ['object', 'null'] }
132
+ }, [
133
+ 'personalSpaceId', 'personalProjectId', 'idempotencyKey', 'title', 'objective',
134
+ 'workKind', 'routingReason'
135
+ ]);
136
+ export const projectAgentTaskCoordinateInput = objectSchema({
137
+ projectPath: boundedString(4096),
138
+ idempotencyKey,
139
+ title: boundedString(160),
140
+ objective: boundedString(4096),
141
+ workKind: boundedString(128),
142
+ requiredCapabilities: arraySchema(boundedString(512), { maxItems: 16 }),
143
+ duration: projectAgentTaskDuration,
144
+ staffingIntent: projectAgentStaffingIntent,
145
+ leadAgentId: nullableStringSchema(),
146
+ collaboratorAgentIds: arraySchema(id, { maxItems: 16 }),
147
+ coordinatorAgentId: nullableStringSchema(),
148
+ complexityHint: { ...projectAgentTaskComplexity, type: ['string', 'null'] },
149
+ parallelPlan: projectAgentParallelPlan,
150
+ modelStrategyOverride: { ...projectAgentProfile.properties.defaultModelStrategy, type: ['object', 'null'] },
151
+ executorPolicyOverride: { ...projectAgentExecutorPolicy, type: ['object', 'null'] },
152
+ routingReason: shortText,
153
+ recruitmentProfile: { ...projectAgentProfile, type: ['object', 'null'] },
154
+ contextQueries: arraySchema(shortText, { minItems: 1, maxItems: 10 }),
155
+ contextLimitPerQuery: integerSchema({ minimum: 1, maximum: 20 }),
156
+ includePendingContext: booleanSchema()
157
+ }, [
158
+ 'projectPath', 'idempotencyKey', 'title', 'objective', 'workKind',
159
+ 'routingReason', 'contextQueries'
160
+ ]);
161
+ export const projectAgentTaskActivityInput = objectSchema({
162
+ personalSpaceId: id,
163
+ personalProjectId: id,
164
+ taskId: id,
165
+ idempotencyKey,
166
+ expectedRevision: integerSchema({ minimum: 0 }),
167
+ status: projectAgentTaskStatus,
168
+ summary: shortText,
169
+ agentId: nullableStringSchema(),
170
+ actorKind: enumSchema(['agent', 'human']),
171
+ sourceApplication,
172
+ sourceSessionId: nullableStringSchema(),
173
+ actualExecutor: nullableStringSchema(),
174
+ actualModelProvider: nullableStringSchema(),
175
+ actualModel: nullableStringSchema(),
176
+ routingRuleId: nullableStringSchema(),
177
+ fallbackReason: nullableStringSchema(),
178
+ actualExecutorId: nullableStringSchema(),
179
+ matchedExecutorRuleId: nullableStringSchema(),
180
+ executorSelectionReason: nullableStringSchema(),
181
+ executorFallbackReason: nullableStringSchema(),
182
+ executorBlockedReason: nullableStringSchema(),
183
+ workerId: { ...nullableStringSchema(), minLength: 1, maxLength: 128 },
184
+ workerLabel: { ...nullableStringSchema(), minLength: 1, maxLength: 160 },
185
+ workerOccupationEmoji: { ...nullableStringSchema(), minLength: 1, maxLength: 32 },
186
+ workerStatus: { ...projectAgentTaskStatus, type: ['string', 'null'] }
187
+ }, [
188
+ 'personalSpaceId', 'personalProjectId', 'taskId', 'idempotencyKey', 'status', 'summary'
189
+ ]);
190
+ export const recruitmentConfirmationMode = enumSchema(['automatic', 'require_confirmation']);
191
+ const recruitmentPositionKind = enumSchema(['durable', 'temporary']);
192
+ export const recruitmentStatus = enumSchema([
193
+ 'awaiting_confirmation', 'requested', 'fulfilled', 'cancelled', 'blocked', 'no_hr'
194
+ ]);
195
+ export const recruitmentDecision = enumSchema(['approve', 'cancel']);
196
+ const executorRegistrationStatus = enumSchema(['registered', 'disabled', 'revoked']);
197
+ const executorPermissionStatus = enumSchema(['pending', 'authorized', 'denied', 'revoked']);
198
+ const executorPreflightStatus = enumSchema(['not_run', 'passed', 'failed', 'expired']);
199
+ export const executorHealthStatus = enumSchema(['unknown', 'healthy', 'degraded', 'unhealthy']);
200
+ export const executorAuthorizationStatus = enumSchema(['pending', 'authorized', 'denied', 'revoked']);
201
+ const executorActualModelStrategySource = enumSchema([
202
+ 'task', 'assignment', 'agent', 'routing_rule', 'coordinator'
203
+ ]);
204
+ export const executorModel = objectSchema({
205
+ provider: boundedString(128),
206
+ model: boundedString(256),
207
+ capabilities: arraySchema(boundedString(512), { maxItems: 32 }),
208
+ strategyModes: arraySchema(enumSchema(['adaptive', 'fast', 'balanced', 'deep']), { maxItems: 4 }),
209
+ reasoningEfforts: arraySchema(enumSchema(['default', 'low', 'medium', 'high']), { maxItems: 4 }),
210
+ available: booleanSchema(),
211
+ observedAt: nullableStringSchema(),
212
+ sourceApplication,
213
+ unavailableReason: nullableStringSchema()
214
+ }, ['provider', 'model']);
215
+ export const executorProfile = objectSchema({
216
+ personalSpaceId: id,
217
+ executorId: id,
218
+ displayName: boundedString(160),
219
+ executorKind: boundedString(128),
220
+ capabilities: arraySchema(boundedString(512), { maxItems: 32 }),
221
+ advertisedModels: arraySchema(executorModel, { maxItems: 64 }),
222
+ globalPriority: integerSchema({ minimum: 1, maximum: 1000000 }),
223
+ healthRequired: booleanSchema(),
224
+ expectedRevision: integerSchema({ minimum: 0 }),
225
+ idempotencyKey,
226
+ sourceApplication,
227
+ sourceSessionId: nullableStringSchema(),
228
+ testSource: nullableStringSchema(),
229
+ cleanupEligible: booleanSchema()
230
+ }, ['personalSpaceId', 'executorId', 'displayName', 'idempotencyKey']);
231
+ export const routingRuleScope = enumSchema(['global', 'space', 'project', 'task']);
232
+ export const executorRoutingRuleInput = objectSchema({
233
+ personalSpaceId: id,
234
+ scope: routingRuleScope,
235
+ personalProjectId: nullableStringSchema(),
236
+ taskId: nullableStringSchema(),
237
+ workKind: boundedString(128),
238
+ requiredCapabilities: arraySchema(boundedString(512), { maxItems: 32 }),
239
+ executorIds: arraySchema(id, { minItems: 1, maxItems: 32 }),
240
+ modelStrategy: { ...projectAgentProfile.properties.defaultModelStrategy, type: ['object', 'null'] },
241
+ priority: integerSchema({ minimum: 1, maximum: 1000000 }),
242
+ reason: shortText,
243
+ idempotencyKey
244
+ }, ['scope', 'workKind', 'reason', 'idempotencyKey']);
245
+ export const executorActualReportInput = objectSchema({
246
+ personalSpaceId: id,
247
+ personalProjectId: id,
248
+ taskId: id,
249
+ runId: id,
250
+ agentId: id,
251
+ executorId: id,
252
+ provider: boundedString(128),
253
+ model: boundedString(256),
254
+ modelStrategy: projectAgentProfile.properties.defaultModelStrategy,
255
+ modelStrategySource: executorActualModelStrategySource,
256
+ matchedRuleId: nullableStringSchema(),
257
+ fallbackReason: nullableStringSchema(),
258
+ idempotencyKey,
259
+ occurredAt: dateTime,
260
+ sourceApplication,
261
+ sourceSessionId: nullableStringSchema()
262
+ }, [
263
+ 'personalSpaceId', 'personalProjectId', 'taskId', 'runId', 'agentId',
264
+ 'executorId', 'provider', 'model', 'idempotencyKey', 'occurredAt'
265
+ ]);
266
+ const outcomeEvidenceKind = enumSchema([
267
+ 'terminal_outcome',
268
+ 'rework_requested', 'repeated_negative_feedback', 'explicit_praise',
269
+ 'test_passed', 'test_failed', 'acceptance_passed', 'acceptance_failed', 'explicit_rating'
270
+ ]);
271
+ const outcomeEvidenceSource = enumSchema(['system_terminal', 'user_explicit', 'test_fact']);
272
+ const outcomeTerminalStatus = enumSchema(['completed', 'failed', 'cancelled']);
273
+ export const projectAgentTaskOutcomeInput = objectSchema({
274
+ personalSpaceId: id,
275
+ personalProjectId: id,
276
+ workKind: boundedString(128),
277
+ agentId: id,
278
+ executorId: id,
279
+ taskId: id,
280
+ runId: nullableStringSchema(),
281
+ idempotencyKey,
282
+ modelStrategy: { ...projectAgentProfile.properties.defaultModelStrategy, type: ['object', 'null'] },
283
+ evidenceKind: outcomeEvidenceKind,
284
+ source: outcomeEvidenceSource,
285
+ terminalOutcome: { ...outcomeTerminalStatus, type: ['string', 'null'] },
286
+ rating: { ...integerSchema({ minimum: 1, maximum: 5 }), type: ['integer', 'null'] },
287
+ referenceIds: arraySchema(id, { maxItems: 16 }),
288
+ note: nullableStringSchema(),
289
+ occurredAt: dateTime
290
+ }, [
291
+ 'personalSpaceId', 'personalProjectId', 'workKind', 'agentId', 'executorId', 'taskId',
292
+ 'idempotencyKey', 'evidenceKind', 'source', 'occurredAt'
293
+ ]);
294
+
295
+ // Keep the complete state vocabulary in one place even when a state schema is
296
+ // currently represented only in Provider responses rather than tool inputs.
297
+ export const projectAgentResponseStateSchemas = {
298
+ recruitmentPositionKind,
299
+ executorRegistrationStatus,
300
+ executorPermissionStatus,
301
+ executorPreflightStatus,
302
+ executorHealthStatus
303
+ };
304
+
305
+ function boundedString(maxLength) {
306
+ return { ...stringSchema(), minLength: 1, maxLength };
307
+ }
@@ -0,0 +1,289 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import {
4
+ chmod,
5
+ lstat,
6
+ mkdir,
7
+ readFile,
8
+ rename,
9
+ rm,
10
+ writeFile
11
+ } from 'node:fs/promises';
12
+ import { basename, dirname, join, resolve } from 'node:path';
13
+
14
+ import { FULI_VERSION } from '../package-metadata.js';
15
+
16
+ const BACKUP_FORMAT = 'fuli-neo4j-backup';
17
+ const BACKUP_VERSION = 1;
18
+
19
+ export async function exportGraphData({
20
+ outputDir,
21
+ sourceMode,
22
+ instances,
23
+ adapter,
24
+ now = () => new Date()
25
+ }) {
26
+ const destination = requiredPath(outputDir, 'Backup output directory');
27
+ validateMode(sourceMode, 'sourceMode');
28
+ const selectedInstances = validateInstances(instances);
29
+ assertAdapter(adapter, ['stop', 'dump', 'start']);
30
+ if (await pathExists(destination)) {
31
+ throw new Error('Backup output already exists; choose a new directory.');
32
+ }
33
+ await mkdir(dirname(destination), { recursive: true });
34
+ const partial = `${destination}.partial-${randomUUID()}`;
35
+ await mkdir(partial, { recursive: false, mode: 0o700 });
36
+ let lifecycle = null;
37
+ let published = false;
38
+ try {
39
+ lifecycle = await adapter.stop();
40
+ const exported = [];
41
+ for (const id of selectedInstances) {
42
+ const file = `${id}.dump`;
43
+ const path = join(partial, file);
44
+ await adapter.dump(id, path);
45
+ await chmod(path, 0o600);
46
+ exported.push({ id, file, sha256: await sha256File(path) });
47
+ }
48
+ const manifest = {
49
+ format: BACKUP_FORMAT,
50
+ version: BACKUP_VERSION,
51
+ createdAt: now().toISOString(),
52
+ sourceMode,
53
+ fuliVersion: FULI_VERSION,
54
+ instances: exported,
55
+ ...(typeof adapter.selection === 'function'
56
+ ? selectionProperty(await adapter.selection())
57
+ : {})
58
+ };
59
+ const manifestPath = join(partial, 'manifest.json');
60
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
61
+ await rename(partial, destination);
62
+ published = true;
63
+ return {
64
+ status: 'exported',
65
+ outputDir: destination,
66
+ sourceMode,
67
+ instances: selectedInstances
68
+ };
69
+ } finally {
70
+ if (!published) await rm(partial, { recursive: true, force: true });
71
+ if (lifecycle?.resume === true) await adapter.start(lifecycle);
72
+ }
73
+ }
74
+
75
+ export async function importGraphData({
76
+ inputDir,
77
+ targetMode,
78
+ rollbackDir,
79
+ adapter,
80
+ now = () => new Date()
81
+ }) {
82
+ const source = requiredPath(inputDir, 'Backup input directory');
83
+ const rollbackBase = requiredPath(rollbackDir, 'Rollback directory');
84
+ validateMode(targetMode, 'targetMode');
85
+ assertAdapter(adapter, ['stop', 'dump', 'load', 'start']);
86
+ const manifest = await readGraphBackupManifest(source);
87
+ await verifyBackupFiles(source, manifest);
88
+ if (
89
+ Array.isArray(adapter.instances) &&
90
+ manifest.instances.some(({ id }) => !adapter.instances.includes(id))
91
+ ) {
92
+ throw new Error(
93
+ 'The target runtime is not configured for every graph instance in this backup.'
94
+ );
95
+ }
96
+
97
+ const rollbackRoot = join(
98
+ rollbackBase,
99
+ `before-import-${now().toISOString().replace(/[:.]/g, '-')}-${randomUUID()}`
100
+ );
101
+ await mkdir(rollbackRoot, { recursive: true, mode: 0o700 });
102
+ let lifecycle = null;
103
+ const rollbackEntries = [];
104
+ let rollbackReady = false;
105
+ try {
106
+ lifecycle = await adapter.stop();
107
+ for (const entry of manifest.instances) {
108
+ if (typeof adapter.hasData === 'function' && !await adapter.hasData(entry.id)) continue;
109
+ const path = join(rollbackRoot, `${entry.id}.dump`);
110
+ await adapter.dump(entry.id, path);
111
+ await chmod(path, 0o600);
112
+ rollbackEntries.push({ id: entry.id, path });
113
+ }
114
+ if (rollbackEntries.length > 0) {
115
+ const rollbackManifest = {
116
+ format: BACKUP_FORMAT,
117
+ version: BACKUP_VERSION,
118
+ createdAt: now().toISOString(),
119
+ sourceMode: targetMode,
120
+ fuliVersion: FULI_VERSION,
121
+ reason: 'automatic-pre-import-rollback',
122
+ instances: await Promise.all(rollbackEntries.map(async ({ id, path }) => ({
123
+ id,
124
+ file: basename(path),
125
+ sha256: await sha256File(path)
126
+ })))
127
+ };
128
+ await writeFile(join(rollbackRoot, 'manifest.json'),
129
+ `${JSON.stringify(rollbackManifest, null, 2)}\n`, { mode: 0o600 });
130
+ rollbackReady = true;
131
+ } else {
132
+ await rm(rollbackRoot, { recursive: true, force: true });
133
+ }
134
+ try {
135
+ for (const entry of manifest.instances) {
136
+ await adapter.load(entry.id, join(source, entry.file), { rollback: false });
137
+ }
138
+ if (typeof adapter.reconcile === 'function') {
139
+ await adapter.reconcile({
140
+ selection: manifest.selection ?? {},
141
+ sourceMode: manifest.sourceMode,
142
+ targetMode,
143
+ lifecycle
144
+ });
145
+ }
146
+ } catch (error) {
147
+ let rollbackError = null;
148
+ for (const entry of rollbackEntries) {
149
+ try {
150
+ await adapter.load(entry.id, entry.path, { rollback: true });
151
+ } catch (caught) {
152
+ rollbackError ??= caught;
153
+ }
154
+ }
155
+ if (rollbackError) {
156
+ throw new AggregateError([error, rollbackError],
157
+ 'Graph import failed and its automatic rollback was incomplete.');
158
+ }
159
+ throw error;
160
+ }
161
+ return {
162
+ status: 'imported',
163
+ inputDir: source,
164
+ targetMode,
165
+ instances: manifest.instances.map(({ id }) => id),
166
+ rollbackDir: rollbackReady ? rollbackRoot : null
167
+ };
168
+ } catch (error) {
169
+ if (!rollbackReady) await rm(rollbackRoot, { recursive: true, force: true });
170
+ throw error;
171
+ } finally {
172
+ if (lifecycle?.resume === true) await adapter.start(lifecycle);
173
+ }
174
+ }
175
+
176
+ export async function readGraphBackupManifest(inputDir) {
177
+ const root = requiredPath(inputDir, 'Backup input directory');
178
+ let manifest;
179
+ try {
180
+ manifest = JSON.parse(await readFile(join(root, 'manifest.json'), 'utf8'));
181
+ } catch (error) {
182
+ throw new Error('Backup manifest is missing or invalid.', { cause: error });
183
+ }
184
+ if (manifest?.format !== BACKUP_FORMAT || manifest.version !== BACKUP_VERSION) {
185
+ throw new Error('Backup format or version is not supported.');
186
+ }
187
+ validateMode(manifest.sourceMode, 'backup source mode');
188
+ validateManifestInstances(manifest.instances);
189
+ validateSelection(manifest.selection);
190
+ return manifest;
191
+ }
192
+
193
+ async function verifyBackupFiles(root, manifest) {
194
+ for (const entry of manifest.instances) {
195
+ const actual = await sha256File(join(root, entry.file));
196
+ if (actual !== entry.sha256) {
197
+ throw new Error(`Backup checksum validation failed for ${entry.id}.`);
198
+ }
199
+ }
200
+ }
201
+
202
+ function validateManifestInstances(instances) {
203
+ if (!Array.isArray(instances) || instances.length === 0) {
204
+ throw new Error('Backup manifest does not contain graph instances.');
205
+ }
206
+ const ids = validateInstances(instances.map(({ id }) => id));
207
+ if (ids.length !== instances.length) throw new Error('Backup instances are invalid.');
208
+ for (const entry of instances) {
209
+ if (
210
+ entry.file !== `${entry.id}.dump` ||
211
+ basename(entry.file) !== entry.file ||
212
+ !/^[a-f0-9]{64}$/.test(entry.sha256)
213
+ ) {
214
+ throw new Error(`Backup entry for ${entry.id} is invalid.`);
215
+ }
216
+ }
217
+ }
218
+
219
+ function validateSelection(selection) {
220
+ if (selection === undefined) return;
221
+ if (
222
+ !selection ||
223
+ typeof selection !== 'object' ||
224
+ Object.keys(selection).some((key) => key !== 'personalSpaceId') ||
225
+ typeof selection.personalSpaceId !== 'string' ||
226
+ !selection.personalSpaceId.trim() ||
227
+ selection.personalSpaceId.length > 200 ||
228
+ /[\s\u0000-\u001f\u007f]/u.test(selection.personalSpaceId)
229
+ ) {
230
+ throw new Error('Backup personal-space selection is invalid.');
231
+ }
232
+ }
233
+
234
+ function selectionProperty(selection) {
235
+ if (selection == null) return {};
236
+ validateSelection(selection);
237
+ return { selection };
238
+ }
239
+
240
+ function validateInstances(instances) {
241
+ if (!Array.isArray(instances) || instances.length === 0) {
242
+ throw new TypeError('At least one graph instance is required.');
243
+ }
244
+ const normalized = [...new Set(instances)];
245
+ if (
246
+ normalized.length !== instances.length ||
247
+ normalized.some((id) => !['personal', 'workspace'].includes(id))
248
+ ) {
249
+ throw new TypeError('Graph instances must be unique personal or workspace entries.');
250
+ }
251
+ return normalized;
252
+ }
253
+
254
+ function validateMode(mode, label) {
255
+ if (!['container', 'native'].includes(mode)) {
256
+ throw new TypeError(`${label} must be container or native.`);
257
+ }
258
+ }
259
+
260
+ function assertAdapter(adapter, methods) {
261
+ if (!adapter || methods.some((method) => typeof adapter[method] !== 'function')) {
262
+ throw new TypeError('A complete graph backup runtime adapter is required.');
263
+ }
264
+ }
265
+
266
+ function requiredPath(value, label) {
267
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required.`);
268
+ return resolve(value);
269
+ }
270
+
271
+ async function pathExists(path) {
272
+ try {
273
+ await lstat(path);
274
+ return true;
275
+ } catch (error) {
276
+ if (error?.code === 'ENOENT') return false;
277
+ throw error;
278
+ }
279
+ }
280
+
281
+ function sha256File(path) {
282
+ return new Promise((resolveHash, reject) => {
283
+ const hash = createHash('sha256');
284
+ const stream = createReadStream(path);
285
+ stream.on('error', reject);
286
+ stream.on('data', (chunk) => hash.update(chunk));
287
+ stream.on('end', () => resolveHash(hash.digest('hex')));
288
+ });
289
+ }