fuli-context 0.7.4-beta.2 → 0.7.5

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 (133) hide show
  1. package/README.md +93 -7
  2. package/README.zh-CN.md +84 -7
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
  9. package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
  11. package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
  17. package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
  18. package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
  19. package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
  22. package/dist/web/assets/{console-DMvHxKf9.js → console-BlAhujsH.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-Dnlcw7Rj.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
  24. package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
  25. package/dist/web/assets/{identity-C9mLdR8L.js → identity-4_-v6cB7.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-C67Q0JY2.js +2 -0
  28. package/dist/web/assets/{model-CsYAR8O3.js → model-yG0YqUyZ.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-BLQiPVw0.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
  30. package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/docs/public-personal-architecture.md +73 -0
  33. package/graph-provider/fuli_graph/app.py +8 -0
  34. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  35. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  36. package/graph-provider/fuli_graph/model_validation.py +134 -0
  37. package/graph-provider/fuli_graph/models.py +24 -5
  38. package/graph-provider/fuli_graph/project_agent_access.py +54 -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 +597 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +542 -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_executor_learning.py +756 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  50. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
  53. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
  54. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  55. package/graph-provider/pyproject.toml +2 -1
  56. package/npm-shrinkwrap.json +2 -2
  57. package/package.json +2 -1
  58. package/skills/capturing-session-knowledge/SKILL.md +43 -2
  59. package/src/adaptive-runtime/executor-pool.js +127 -0
  60. package/src/adaptive-runtime/graph-services.js +166 -0
  61. package/src/adaptive-runtime/lease-client.js +335 -0
  62. package/src/adaptive-runtime/runtime-broker.js +318 -0
  63. package/src/adaptive-runtime/settings.js +111 -0
  64. package/src/adaptive-runtime/state.js +74 -0
  65. package/src/agent-tools/graph-definitions.js +396 -0
  66. package/src/agent-tools/graph-handlers.js +61 -0
  67. package/src/agent-tools/project-agent-definitions.js +307 -0
  68. package/src/backup/graph-backup.js +289 -0
  69. package/src/backup/identity-reconciliation.js +208 -0
  70. package/src/backup/runtime-adapters.js +269 -0
  71. package/src/cli/command-registry.js +7 -2
  72. package/src/cli/graph-data-command.js +112 -0
  73. package/src/cli/local-runtime-command.js +16 -2
  74. package/src/cli/setup-command.js +19 -3
  75. package/src/cli/update-command.js +4 -0
  76. package/src/cli/workspace-connection-command.js +333 -0
  77. package/src/cli.js +11 -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 +74 -112
  81. package/src/graphiti/knowledge-provider-mapping.js +7 -1
  82. package/src/graphiti/project-agent-control-plane.js +263 -0
  83. package/src/graphiti/project-agent-mapping.js +1003 -0
  84. package/src/graphiti/project-agent-workflows.js +557 -0
  85. package/src/graphiti/provider-client.js +270 -1
  86. package/src/graphiti/runtime-config.js +6 -1
  87. package/src/graphiti/workspace-provider-client.js +403 -0
  88. package/src/http/api-router.js +63 -9
  89. package/src/http/graph-api-router.js +278 -0
  90. package/src/local-runtime/lifecycle.js +46 -10
  91. package/src/mcp/context-resources.js +15 -6
  92. package/src/mcp/create-mcp-server.js +51 -16
  93. package/src/mcp/instructions.js +7 -5
  94. package/src/mcp/runtime.js +17 -5
  95. package/src/mcp/session-id.js +38 -1
  96. package/src/mcp/tool-annotations.js +69 -0
  97. package/src/mcp-server.js +23 -6
  98. package/src/native-runtime/runtime.js +717 -0
  99. package/src/server.js +6 -2
  100. package/src/setup/agent-installation-status.js +7 -1
  101. package/src/setup/agents.js +3 -1
  102. package/src/setup/claude-code-config.js +3 -1
  103. package/src/setup/codex-bootstrap.js +11 -10
  104. package/src/setup/codex-config.js +7 -1
  105. package/src/setup/cursor-config.js +3 -1
  106. package/src/setup/graph-runtime.js +158 -13
  107. package/src/setup/neo4j-memory-profile.js +43 -0
  108. package/src/setup/options.js +38 -2
  109. package/src/setup/paths.js +14 -0
  110. package/src/setup/setup.js +37 -1
  111. package/src/system/resource-monitor.js +181 -16
  112. package/src/system/runtime-settings.js +10 -1
  113. package/src/system/system-service.js +27 -2
  114. package/dist/web/assets/AboutPage-CftRexSY.js +0 -1
  115. package/dist/web/assets/ConnectionsPage-D5J91x6d.js +0 -1
  116. package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +0 -1
  119. package/dist/web/assets/KnowledgePage-OUT07aKH.js +0 -1
  120. package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +0 -4
  121. package/dist/web/assets/OverviewPage-CHE6XhdS.js +0 -1
  122. package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +0 -1
  123. package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +0 -1
  124. package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +0 -1
  125. package/dist/web/assets/ReviewPage-DUeyu7E8.js +0 -1
  126. package/dist/web/assets/SearchableSelect-C5RiB5F8.js +0 -1
  127. package/dist/web/assets/SettingsPage-BMuASSzQ.js +0 -1
  128. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  129. package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +0 -1
  130. package/dist/web/assets/i18n-CWfd8Dtt.js +0 -5
  131. package/dist/web/assets/index-X2j29j8i.css +0 -1
  132. package/dist/web/assets/index-nP5p5OaY.js +0 -2
  133. package/dist/web/assets/vue-router-DN8x6iY1.js +0 -1
@@ -36,13 +36,67 @@ export async function handleApiRequest({
36
36
  return true;
37
37
  }
38
38
 
39
- if (await handleExternalKnowledgeApiRequest({
40
- request,
41
- response,
42
- url,
43
- externalKnowledge,
44
- connectedKnowledge
45
- })) return true;
46
-
47
- return handleGraphApiRequest({ request, response, url, app });
39
+ if (system && url.pathname === '/api/system/runtime' && request.method === 'GET') {
40
+ sendJson(response, 200, system.runtimeStatus());
41
+ return true;
42
+ }
43
+
44
+ if (system && url.pathname === '/api/system/runtime/leases' && request.method === 'POST') {
45
+ sendJson(response, 201, await system.acquireRuntimeLease(await readJson(request)));
46
+ return true;
47
+ }
48
+
49
+ const runtimeLease = url.pathname.match(/^\/api\/system\/runtime\/leases\/([^/]+)$/);
50
+ if (system && runtimeLease && request.method === 'PATCH') {
51
+ sendJson(
52
+ response,
53
+ 200,
54
+ system.refreshRuntimeLease(decodeURIComponent(runtimeLease[1]))
55
+ );
56
+ return true;
57
+ }
58
+ if (system && runtimeLease && request.method === 'DELETE') {
59
+ sendJson(
60
+ response,
61
+ 200,
62
+ system.releaseRuntimeLease(decodeURIComponent(runtimeLease[1]))
63
+ );
64
+ return true;
65
+ }
66
+
67
+ const externalRequest = () => handleExternalKnowledgeApiRequest({
68
+ request, response, url, externalKnowledge, connectedKnowledge
69
+ });
70
+ const handledExternal = system?.withGraphRuntimeLease &&
71
+ externalRequestNeedsGraph(url.pathname, request.method)
72
+ ? await system.withGraphRuntimeLease(
73
+ `http:${request.method}:${url.pathname}`,
74
+ externalRequest
75
+ )
76
+ : await externalRequest();
77
+ if (handledExternal) return true;
78
+
79
+ const graphRequest = () => handleGraphApiRequest({ request, response, url, app });
80
+ if (system?.withGraphRuntimeLease && url.pathname.startsWith('/api/')) {
81
+ return system.withGraphRuntimeLease(
82
+ `http:${request.method}:${url.pathname}`,
83
+ graphRequest
84
+ );
85
+ }
86
+ return graphRequest();
87
+ }
88
+
89
+ function externalRequestNeedsGraph(pathname, method) {
90
+ if (pathname === '/api/connected-knowledge/search' && method === 'POST') return true;
91
+ if (pathname === '/api/external-knowledge/bindings' && method === 'POST') return true;
92
+ if (
93
+ /^\/api\/external-knowledge\/bindings\/[^/]+\/sync$/.test(pathname) &&
94
+ method === 'POST'
95
+ ) return true;
96
+ if (
97
+ /^\/api\/external-knowledge\/bindings\/[^/]+\/targets$/.test(pathname) &&
98
+ method === 'PATCH'
99
+ ) return true;
100
+ return /^\/api\/external-knowledge\/bindings\/[^/]+$/.test(pathname) &&
101
+ method === 'DELETE';
48
102
  }
@@ -178,6 +178,284 @@ export async function handleGraphApiRequest({
178
178
  sendJson(response, 200, await app.upsertPersonalProject(await readJson(request)));
179
179
  return true;
180
180
  }
181
+ if (url.pathname === '/api/project-agents' && request.method === 'GET') {
182
+ sendJson(response, 200, await app.listProjectAgents({
183
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
184
+ personalProjectId: url.searchParams.get('personalProjectId'),
185
+ status: url.searchParams.get('status'),
186
+ capability: url.searchParams.get('capability')
187
+ }));
188
+ return true;
189
+ }
190
+ if (url.pathname === '/api/project-agents' && request.method === 'PUT') {
191
+ sendJson(response, 200, await app.upsertProjectAgent(await readJson(request)));
192
+ return true;
193
+ }
194
+ if (url.pathname === '/api/project-agent-context' && request.method === 'POST') {
195
+ sendJson(response, 200, await app.getProjectAgentContext(await readJson(request)));
196
+ return true;
197
+ }
198
+ const projectAgentPath = url.pathname.match(/^\/api\/project-agents\/([^/]+)$/);
199
+ if (projectAgentPath && request.method === 'GET') {
200
+ sendJson(response, 200, await app.getProjectAgent({
201
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
202
+ personalProjectId: url.searchParams.get('personalProjectId'),
203
+ agentId: decodeURIComponent(projectAgentPath[1])
204
+ }));
205
+ return true;
206
+ }
207
+ if (projectAgentPath && request.method === 'DELETE') {
208
+ sendJson(response, 200, await app.deleteProjectAgent({
209
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
210
+ agentId: decodeURIComponent(projectAgentPath[1]),
211
+ reason: url.searchParams.get('reason') ?? 'archived by user'
212
+ }));
213
+ return true;
214
+ }
215
+ if (url.pathname === '/api/project-agents/test-cleanup' && request.method === 'POST') {
216
+ sendJson(response, 200, await app.cleanupProjectAgentTestRoles({
217
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
218
+ testSource: url.searchParams.get('testSource')
219
+ }));
220
+ return true;
221
+ }
222
+ if (url.pathname === '/api/project-agent-assignments' && request.method === 'GET') {
223
+ sendJson(response, 200, await app.listProjectAgentAssignments({
224
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
225
+ personalProjectId: url.searchParams.get('personalProjectId'),
226
+ agentId: url.searchParams.get('agentId'),
227
+ status: url.searchParams.get('status')
228
+ }));
229
+ return true;
230
+ }
231
+ if (url.pathname === '/api/project-agent-assignments' && request.method === 'POST') {
232
+ sendJson(response, 200, await app.createProjectAgentAssignment(await readJson(request)));
233
+ return true;
234
+ }
235
+ const projectAgentAssignmentPath = url.pathname.match(
236
+ /^\/api\/project-agent-assignments\/([^/]+)\/(end|replace)$/
237
+ );
238
+ if (projectAgentAssignmentPath && request.method === 'POST') {
239
+ const body = await readJson(request);
240
+ const input = {
241
+ ...body,
242
+ assignmentId: decodeURIComponent(projectAgentAssignmentPath[1])
243
+ };
244
+ const result = projectAgentAssignmentPath[2] === 'end'
245
+ ? await app.endProjectAgentAssignment(input)
246
+ : await app.replaceProjectAgentAssignment(input);
247
+ sendJson(response, 200, result);
248
+ return true;
249
+ }
250
+ if (url.pathname === '/api/project-agent-tasks' && request.method === 'GET') {
251
+ sendJson(response, 200, await app.listProjectAgentTasks({
252
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
253
+ personalProjectId: url.searchParams.get('personalProjectId'),
254
+ agentId: url.searchParams.get('agentId'),
255
+ status: url.searchParams.get('status'),
256
+ limit: numberParam(url, 'limit', null)
257
+ }));
258
+ return true;
259
+ }
260
+ if (url.pathname === '/api/project-agent-tasks' && request.method === 'POST') {
261
+ sendJson(response, 200, await app.submitProjectAgentTask(await readJson(request)));
262
+ return true;
263
+ }
264
+ const projectAgentTaskPath = url.pathname.match(/^\/api\/project-agent-tasks\/([^/]+)$/);
265
+ if (projectAgentTaskPath && request.method === 'GET') {
266
+ sendJson(response, 200, await app.viewProjectAgentTask({
267
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
268
+ taskId: decodeURIComponent(projectAgentTaskPath[1]),
269
+ includeEvents: url.searchParams.get('includeEvents') !== 'false'
270
+ }));
271
+ return true;
272
+ }
273
+ if (url.pathname === '/api/project-agent-activity' && request.method === 'GET') {
274
+ sendJson(response, 200, await app.viewProjectAgentActivity({
275
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
276
+ agentId: url.searchParams.get('agentId'),
277
+ fromDate: url.searchParams.get('fromDate'),
278
+ toDate: url.searchParams.get('toDate')
279
+ }));
280
+ return true;
281
+ }
282
+ if (url.pathname === '/api/project-agent-activity' && request.method === 'POST') {
283
+ sendJson(response, 200, await app.recordProjectAgentTaskActivity(await readJson(request)));
284
+ return true;
285
+ }
286
+ if (url.pathname === '/api/project-agent-recruitment-policy' && request.method === 'GET') {
287
+ sendJson(response, 200, await app.getProjectAgentRecruitmentPolicy({
288
+ personalSpaceId: url.searchParams.get('personalSpaceId')
289
+ }));
290
+ return true;
291
+ }
292
+ if (url.pathname === '/api/project-agent-recruitment-policy' &&
293
+ (request.method === 'PUT' || request.method === 'PATCH')) {
294
+ sendJson(response, 200, await app.updateProjectAgentRecruitmentPolicy(await readJson(request)));
295
+ return true;
296
+ }
297
+ if (url.pathname === '/api/project-agent-recruitments' && request.method === 'GET') {
298
+ sendJson(response, 200, await app.listProjectAgentRecruitments({
299
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
300
+ personalProjectId: url.searchParams.get('personalProjectId'),
301
+ taskId: url.searchParams.get('taskId'),
302
+ status: url.searchParams.get('status')
303
+ }));
304
+ return true;
305
+ }
306
+ if (url.pathname === '/api/project-agent-recruitments/decision' && request.method === 'POST') {
307
+ sendJson(response, 200, await app.decideProjectAgentRecruitment(await readJson(request)));
308
+ return true;
309
+ }
310
+ if (url.pathname === '/api/executors' && request.method === 'GET') {
311
+ sendJson(response, 200, await app.listExecutors({
312
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
313
+ capability: url.searchParams.get('capability'),
314
+ availableOnly: url.searchParams.get('availableOnly') === 'true'
315
+ }));
316
+ return true;
317
+ }
318
+ if (url.pathname === '/api/executors' && request.method === 'PUT') {
319
+ sendJson(response, 200, await app.upsertExecutor(await readJson(request)));
320
+ return true;
321
+ }
322
+ const executorPath = url.pathname.match(/^\/api\/executors\/([^/]+)$/);
323
+ if (executorPath && request.method === 'GET') {
324
+ sendJson(response, 200, await app.getExecutor({
325
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
326
+ executorId: decodeURIComponent(executorPath[1])
327
+ }));
328
+ return true;
329
+ }
330
+ if (executorPath && request.method === 'DELETE') {
331
+ sendJson(response, 200, await app.deleteExecutor({
332
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
333
+ executorId: decodeURIComponent(executorPath[1])
334
+ }));
335
+ return true;
336
+ }
337
+ if (url.pathname === '/api/executors/preflight' && request.method === 'POST') {
338
+ sendJson(response, 200, await app.preflightExecutor(await readJson(request)));
339
+ return true;
340
+ }
341
+ if (url.pathname === '/api/executors/authorization' && request.method === 'POST') {
342
+ sendJson(response, 200, await app.authorizeExecutor(await readJson(request)));
343
+ return true;
344
+ }
345
+ if (url.pathname === '/api/executors/health' && request.method === 'POST') {
346
+ sendJson(response, 200, await app.reportExecutorHealth(await readJson(request)));
347
+ return true;
348
+ }
349
+ if (url.pathname === '/api/project-agent-executor-actuals' && request.method === 'POST') {
350
+ sendJson(response, 200, await app.recordProjectAgentExecutorActual(await readJson(request)));
351
+ return true;
352
+ }
353
+ if (url.pathname === '/api/executor-routing-rules' && request.method === 'GET') {
354
+ sendJson(response, 200, await app.listExecutorRoutingRules({
355
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
356
+ scope: url.searchParams.get('scope'),
357
+ personalProjectId: url.searchParams.get('personalProjectId'),
358
+ taskId: url.searchParams.get('taskId'),
359
+ status: url.searchParams.get('status')
360
+ }));
361
+ return true;
362
+ }
363
+ if (url.pathname === '/api/executor-routing-rules' && request.method === 'PUT') {
364
+ sendJson(response, 200, await app.upsertExecutorRoutingRule(await readJson(request)));
365
+ return true;
366
+ }
367
+ const executorRoutingRulePath = url.pathname.match(
368
+ /^\/api\/executor-routing-rules\/([^/]+)$/
369
+ );
370
+ if (executorRoutingRulePath && request.method === 'GET') {
371
+ sendJson(response, 200, await app.getExecutorRoutingRule({
372
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
373
+ ruleId: decodeURIComponent(executorRoutingRulePath[1])
374
+ }));
375
+ return true;
376
+ }
377
+ if (executorRoutingRulePath && request.method === 'PATCH') {
378
+ const body = await readJson(request);
379
+ sendJson(response, 200, await app.updateExecutorRoutingRule({
380
+ ...body,
381
+ personalSpaceId: body.personalSpaceId ?? url.searchParams.get('personalSpaceId'),
382
+ ruleId: decodeURIComponent(executorRoutingRulePath[1])
383
+ }));
384
+ return true;
385
+ }
386
+ if (executorRoutingRulePath && request.method === 'DELETE') {
387
+ sendJson(response, 200, await app.deleteExecutorRoutingRule({
388
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
389
+ ruleId: decodeURIComponent(executorRoutingRulePath[1])
390
+ }));
391
+ return true;
392
+ }
393
+ if (url.pathname === '/api/project-agent-routing-learning' && request.method === 'GET') {
394
+ sendJson(response, 200, await app.listProjectAgentRoutingLearning({
395
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
396
+ personalProjectId: url.searchParams.get('personalProjectId'),
397
+ workKind: url.searchParams.get('workKind'),
398
+ agentId: url.searchParams.get('agentId'),
399
+ executorId: url.searchParams.get('executorId')
400
+ }));
401
+ return true;
402
+ }
403
+ const projectAgentLearningPath = url.pathname.match(
404
+ /^\/api\/project-agent-learning\/([^/]+)$/
405
+ );
406
+ if (projectAgentLearningPath && request.method === 'PATCH') {
407
+ const body = await readJson(request);
408
+ const evidenceId = decodeURIComponent(projectAgentLearningPath[1]);
409
+ const action = body.action;
410
+ if (!['ignore', 'reset'].includes(action)) {
411
+ throw new TypeError('Project Agent learning action must be ignore or reset');
412
+ }
413
+ const personalSpaceId = body.personalSpaceId ??
414
+ url.searchParams.get('personalSpaceId') ?? app.config?.personal?.spaceId;
415
+ const personalProjectId = body.personalProjectId ??
416
+ url.searchParams.get('personalProjectId');
417
+ const agentId = body.agentId ?? url.searchParams.get('agentId');
418
+ if (!personalSpaceId || !personalProjectId || !agentId) {
419
+ throw new TypeError(
420
+ 'Project Agent learning updates require personalSpaceId, personalProjectId, and agentId'
421
+ );
422
+ }
423
+ const idempotencyKey = body.idempotencyKey ??
424
+ `console-learning:${evidenceId}:${action}`;
425
+ const reason = body.reason ?? `Console requested ${action} for explicit evidence`;
426
+ const input = {
427
+ ...body,
428
+ personalSpaceId,
429
+ personalProjectId,
430
+ agentId,
431
+ evidenceId,
432
+ idempotencyKey,
433
+ reason
434
+ };
435
+ if (action === 'ignore') {
436
+ sendJson(response, 200, await app.ignoreProjectAgentRoutingLearning(input));
437
+ return true;
438
+ }
439
+ if (!input.workKind || !input.executorId || !input.resetAt) {
440
+ throw new TypeError(
441
+ 'Project Agent learning reset requires workKind, executorId, and resetAt'
442
+ );
443
+ }
444
+ sendJson(response, 200, await app.resetProjectAgentRoutingLearning(input));
445
+ return true;
446
+ }
447
+ if (url.pathname === '/api/project-agent-routing-learning/ignore' && request.method === 'POST') {
448
+ sendJson(response, 200, await app.ignoreProjectAgentRoutingLearning(await readJson(request)));
449
+ return true;
450
+ }
451
+ if (url.pathname === '/api/project-agent-routing-learning/reset' && request.method === 'POST') {
452
+ sendJson(response, 200, await app.resetProjectAgentRoutingLearning(await readJson(request)));
453
+ return true;
454
+ }
455
+ if (url.pathname === '/api/project-agent-routing-outcomes' && request.method === 'POST') {
456
+ sendJson(response, 200, await app.recordProjectAgentTaskOutcome(await readJson(request)));
457
+ return true;
458
+ }
181
459
  if (url.pathname === '/api/projects/publish' && request.method === 'POST') {
182
460
  sendJson(response, 200, await app.publishPersonalProject(await readJson(request)));
183
461
  return true;
@@ -7,6 +7,8 @@ import {
7
7
  stopGraphProviders
8
8
  } from '../setup/graph-runtime.js';
9
9
  import { readJsonFile } from '../storage/json-file.js';
10
+ import { readAdaptiveRuntimeSettings } from '../adaptive-runtime/settings.js';
11
+ import { readAdaptiveRuntimeState } from '../adaptive-runtime/state.js';
10
12
  import {
11
13
  DEFAULT_RUNTIME_SETTINGS,
12
14
  managedProviderUrls
@@ -28,6 +30,7 @@ export async function startLocalRuntime(input, dependencies = {}) {
28
30
  port: input.port,
29
31
  lan: input.lan === true,
30
32
  runtimeSettings,
33
+ previousRuntimeMode: input.previousRuntimeMode,
31
34
  personalOnly: !managesDevelopmentWorkspace,
32
35
  buildProviders,
33
36
  noStart: false,
@@ -51,11 +54,14 @@ export async function stopLocalRuntime(input, dependencies = {}) {
51
54
  const managesDevelopmentWorkspace = state?.managedProviders?.includes(
52
55
  'development-workspace'
53
56
  ) || hasManagedDevelopmentWorkspace(config, runtimeSettings);
57
+ const activeRuntimeMode = state?.runtimeSettings?.graphRuntimeMode ??
58
+ runtimeSettings.graphRuntimeMode;
54
59
 
55
60
  let providers = 'not_initialized';
56
61
  if (deps.fileExists(input.paths.graphEnvPath)) {
57
- deps.stopProviders(input.paths, input.paths.graphEnvPath, {
58
- personalOnly: !managesDevelopmentWorkspace
62
+ await deps.stopProviders(input.paths, input.paths.graphEnvPath, {
63
+ personalOnly: !managesDevelopmentWorkspace,
64
+ runtimeMode: activeRuntimeMode
59
65
  });
60
66
  providers = 'stopped';
61
67
  }
@@ -70,11 +76,14 @@ export async function stopLocalRuntime(input, dependencies = {}) {
70
76
  }
71
77
 
72
78
  export async function restartLocalRuntime(input, dependencies = {}) {
79
+ const deps = lifecycleDependencies(dependencies);
80
+ const previousRuntimeMode = deps.readState(input.paths.graphRuntimeStatePath)
81
+ ?.runtimeSettings?.graphRuntimeMode;
73
82
  const stopped = await stopLocalRuntime(input, dependencies);
74
83
  if (stopped.status === 'partial') {
75
84
  throw new Error('Could not safely verify the existing local UI process; restart was refused.');
76
85
  }
77
- const started = await startLocalRuntime(input, dependencies);
86
+ const started = await startLocalRuntime({ ...input, previousRuntimeMode }, dependencies);
78
87
  return { stopped, ...started, status: 'restarted' };
79
88
  }
80
89
 
@@ -90,19 +99,34 @@ export async function inspectLocalRuntime(input, dependencies = {}) {
90
99
  state.pid,
91
100
  state.version
92
101
  );
102
+ const adaptiveSettings = deps.readAdaptiveSettings(pathsValue(
103
+ input.paths.adaptiveRuntimeSettingsPath
104
+ ));
105
+ const adaptiveState = adaptiveSettings.enabled
106
+ ? deps.readAdaptiveState(pathsValue(input.paths.adaptiveRuntimeStatePath))
107
+ : null;
108
+ const intentionallySleeping = adaptiveSettings.enabled &&
109
+ ['provider-sleeping', 'sleeping'].includes(adaptiveState?.stage);
93
110
  const managedUrls = managedProviderUrls(runtimeSettings);
94
111
  const personalUrl = config?.personal?.providerUrl ?? managedUrls.personal;
95
112
  const personal = config
96
- ? await deps.providerHealth(personalUrl)
113
+ ? intentionallySleeping
114
+ ? { url: personalUrl, status: 'sleeping' }
115
+ : await deps.providerHealth(personalUrl)
97
116
  : { url: personalUrl, status: 'not_configured' };
98
- const workspaces = await Promise.all((config?.workspaces ?? []).map(async (workspace) => ({
99
- ...(await deps.providerHealth(workspace.providerUrl)),
100
- managedDevelopment: isManagedDevelopmentUrl(workspace, runtimeSettings)
101
- })));
117
+ const workspaces = await Promise.all((config?.workspaces ?? []).map(async (workspace) => {
118
+ const managedDevelopment = isManagedDevelopmentUrl(workspace, runtimeSettings);
119
+ return {
120
+ ...(intentionallySleeping && managedDevelopment
121
+ ? { url: workspace.providerUrl, status: 'sleeping' }
122
+ : await deps.providerHealth(workspace.providerUrl)),
123
+ managedDevelopment
124
+ };
125
+ }));
102
126
 
103
127
  let status = 'stopped';
104
128
  if (!config && !state) status = 'not_configured';
105
- else if (consoleReady && personal.status === 'ready') status = 'running';
129
+ else if (consoleReady && ['ready', 'sleeping'].includes(personal.status)) status = 'running';
106
130
  else if (processAlive || personal.status === 'ready') status = 'degraded';
107
131
 
108
132
  return {
@@ -120,6 +144,10 @@ export async function inspectLocalRuntime(input, dependencies = {}) {
120
144
  configured: workspaces.length > 0,
121
145
  status: publicProviderStatus(workspaces),
122
146
  providers: workspaces
147
+ },
148
+ adaptiveRuntime: {
149
+ enabled: adaptiveSettings.enabled,
150
+ stage: adaptiveSettings.enabled ? adaptiveState?.stage ?? 'unknown' : 'always-on'
123
151
  }
124
152
  };
125
153
  }
@@ -162,6 +190,10 @@ function lifecycleDependencies(overrides) {
162
190
  stopProviders: stopGraphProviders,
163
191
  consoleHealth: checkLocalConsoleHealth,
164
192
  providerHealth,
193
+ readAdaptiveSettings: (path) => path
194
+ ? readAdaptiveRuntimeSettings(path)
195
+ : { enabled: false },
196
+ readAdaptiveState: (path) => path ? readAdaptiveRuntimeState(path) : null,
165
197
  isProcessAlive,
166
198
  stopProcess,
167
199
  waitForExit,
@@ -173,6 +205,10 @@ function lifecycleDependencies(overrides) {
173
205
  };
174
206
  }
175
207
 
208
+ function pathsValue(value) {
209
+ return typeof value === 'string' && value ? value : null;
210
+ }
211
+
176
212
  function safeReadState(path) {
177
213
  try {
178
214
  return readJsonFile(path, null);
@@ -242,7 +278,7 @@ function isLoopbackConsoleUrl(value) {
242
278
 
243
279
  function publicProviderStatus(workspaces) {
244
280
  if (!workspaces.length) return 'not_connected';
245
- const ready = workspaces.filter(({ status }) => status === 'ready').length;
281
+ const ready = workspaces.filter(({ status }) => ['ready', 'sleeping'].includes(status)).length;
246
282
  if (ready === workspaces.length) return 'ready';
247
283
  return ready > 0 ? 'degraded' : 'unavailable';
248
284
  }
@@ -6,7 +6,9 @@ const RESOURCE_MIME_TYPE = 'text/markdown';
6
6
  const MAX_PROJECT_PURPOSE_LENGTH = 240;
7
7
  const MAX_RESOURCE_TEXT_BYTES = 32 * 1024;
8
8
 
9
- export function registerFuliContextResources(server, app) {
9
+ export function registerFuliContextResources(server, app, {
10
+ withRuntimeLease = (_owner, operation) => operation()
11
+ } = {}) {
10
12
  server.registerResource(
11
13
  'FULI global taste',
12
14
  GLOBAL_TASTE_RESOURCE_URI,
@@ -15,13 +17,17 @@ export function registerFuliContextResources(server, app) {
15
17
  description: 'The user\'s current evidence-backed global taste and working preferences.',
16
18
  mimeType: RESOURCE_MIME_TYPE
17
19
  },
18
- (uri) => readGlobalTasteResource(app, uri)
20
+ (uri) => withRuntimeLease(
21
+ 'mcp-resource:global-taste',
22
+ () => readGlobalTasteResource(app, uri)
23
+ )
19
24
  );
20
25
 
21
26
  const projectResources = new ResourceTemplate(PROJECT_RESOURCE_TEMPLATE, {
22
- list: async () => ({
23
- resources: await listPersonalProjectResources(app)
24
- })
27
+ list: () => withRuntimeLease(
28
+ 'mcp-resource:list-projects',
29
+ async () => ({ resources: await listPersonalProjectResources(app) })
30
+ )
25
31
  });
26
32
  server.registerResource(
27
33
  'fuli-personal-project',
@@ -31,7 +37,10 @@ export function registerFuliContextResources(server, app) {
31
37
  description: 'Select one exact local FULI personal project for this task.',
32
38
  mimeType: RESOURCE_MIME_TYPE
33
39
  },
34
- (uri, variables) => readPersonalProjectResource(app, uri, variables)
40
+ (uri, variables) => withRuntimeLease(
41
+ 'mcp-resource:project',
42
+ () => readPersonalProjectResource(app, uri, variables)
43
+ )
35
44
  );
36
45
  }
37
46
 
@@ -19,7 +19,8 @@ import { jsonSchemaToZod, openObjectSchema } from './tool-schema.js';
19
19
  import {
20
20
  mcpHostSessionId,
21
21
  nativeCodexThreadId,
22
- normalizeAgentSessionInput
22
+ normalizeAgentSessionInput,
23
+ normalizeMcpSourceApplication
23
24
  } from './session-id.js';
24
25
 
25
26
  const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
@@ -33,6 +34,24 @@ const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
33
34
  discover_personal_global_preference_candidates: 32 * 1024,
34
35
  preview_personal_global_preference_decision: 32 * 1024,
35
36
  list_personal_projects: 32 * 1024,
37
+ list_project_agents: 32 * 1024,
38
+ get_project_agent: 32 * 1024,
39
+ list_project_agent_assignments: 32 * 1024,
40
+ get_project_agent_context: 64 * 1024,
41
+ coordinate_project_agent_task: 256 * 1024,
42
+ acquire_runtime_lease: 4 * 1024,
43
+ refresh_runtime_lease: 4 * 1024,
44
+ release_runtime_lease: 4 * 1024,
45
+ view_project_agent_task: 64 * 1024,
46
+ view_project_agent_activity: 64 * 1024,
47
+ list_project_agent_recruitments: 32 * 1024,
48
+ list_executors: 32 * 1024,
49
+ get_executor: 32 * 1024,
50
+ preflight_executor: 32 * 1024,
51
+ record_project_agent_executor_actual: 32 * 1024,
52
+ list_executor_routing_rules: 32 * 1024,
53
+ get_executor_routing_rule: 32 * 1024,
54
+ list_project_agent_routing_learning: 64 * 1024,
36
55
  list_knowledge_review_candidates: 32 * 1024,
37
56
  list_workflow_candidates: 32 * 1024,
38
57
  recommend_next_workflow_steps: 32 * 1024
@@ -40,7 +59,12 @@ const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
40
59
 
41
60
  export function createMcpServer(
42
61
  app,
43
- { env = process.env, clock = () => new Date() } = {}
62
+ {
63
+ env = process.env,
64
+ clock = () => new Date(),
65
+ sourceApplication = nativeCodexThreadId(env) ? 'codex' : 'other',
66
+ withRuntimeLease = (_owner, operation) => operation()
67
+ } = {}
44
68
  ) {
45
69
  const server = new McpServer(
46
70
  { name: 'fuli', version: FULI_VERSION },
@@ -48,15 +72,20 @@ export function createMcpServer(
48
72
  );
49
73
 
50
74
  const nativeThreadId = nativeCodexThreadId(env);
75
+ const authoritativeSourceApplication = normalizeMcpSourceApplication(
76
+ sourceApplication
77
+ );
51
78
  const tools = createToolMap(
52
79
  app,
53
80
  createProjectActionPreviewTokens(),
54
81
  nativeThreadId,
55
82
  mcpHostSessionId(env),
56
- clock
83
+ clock,
84
+ authoritativeSourceApplication,
85
+ withRuntimeLease
57
86
  );
58
87
  for (const tool of tools.values()) registerTool(server, tool);
59
- registerFuliContextResources(server, app);
88
+ registerFuliContextResources(server, app, { withRuntimeLease });
60
89
  registerCallHandler(server, tools);
61
90
  return server;
62
91
  }
@@ -66,24 +95,30 @@ function createToolMap(
66
95
  projectActionPreviews,
67
96
  nativeThreadId,
68
97
  hostSessionId,
69
- clock
98
+ clock,
99
+ sourceApplication,
100
+ withRuntimeLease
70
101
  ) {
71
102
  const commonKnowledgePreviews = createCommonKnowledgePreviewTokens();
72
103
  return new Map(listAgentTools().map((definition) => [definition.name, {
73
104
  definition,
74
105
  schema: jsonSchemaToZod(definition.inputSchema),
75
- invoke: (input) => invokeAgentTool(
76
- app,
77
- definition.name,
78
- normalizeAgentSessionInput(
106
+ invoke: (input) => withRuntimeLease(
107
+ `mcp-tool:${definition.name}`,
108
+ () => invokeAgentTool(
109
+ app,
79
110
  definition.name,
80
- input,
81
- nativeThreadId,
82
- hostSessionId,
83
- clock
84
- ),
85
- projectActionPreviews,
86
- commonKnowledgePreviews
111
+ normalizeAgentSessionInput(
112
+ definition.name,
113
+ input,
114
+ nativeThreadId,
115
+ hostSessionId,
116
+ clock,
117
+ sourceApplication
118
+ ),
119
+ projectActionPreviews,
120
+ commonKnowledgePreviews
121
+ )
87
122
  )
88
123
  }]));
89
124
  }
@@ -1,7 +1,9 @@
1
1
  export const MCP_INSTRUCTIONS = [
2
- 'Each user task: hook-provided task context from begin_task_context. Otherwise, before other tools/answer, call exactly get_collaboration_preferences with projectPath=cwd and taskPrompt=current user request as fallback; never substitute a project action. Apply all effective_preferences: personal-global everywhere, matched project, authorized inheritable parent. For taste/judgment, call get_user_taste_skill; never replaces a user-authored Skill. FULI fuli:// exposes exact projects/global taste; an @ selection is explicit and adds no project. Resolve relevant deferred_conflict first; never guess personalProjectId. Writes enforce actual payload; final text is not compliance.',
3
- 'Before asking user to repeat a stable project fact or method, inspect task_knowledge_recall. On miss call search_current_project_knowledge with focused action, artifact, target-system, or identifier queries; never use the whole conversational request as the only query. Search active child first, then inheritable parent via authorized PART_OF/USES_KNOWLEDGE_FROM, 2 hops; parent prefs need descendants/selected_projects; local_only/RELATED_TO never inherit; authority > distance > weight; equal conflicts defer.',
4
- 'Before saying unknown, call search_knowledge_graph. Use exact IDs for extra projects. RELATED_TO suggests; ask first. Use all_local_confirmed only after consent, then rg only in safe current repo/workspace; never scan outside it. Live state: monitoring/Git MCP. External/public: search_connected_knowledge; agent_decide is response-only.',
5
- 'Use record_decision_trace; record_knowledge_feedback on rejected/outdated/failed items. Batch durable confirmed knowledge with capture_session_knowledge; no secrets/guesses. Finish with checkpoint_task_knowledge: capture_candidates or retain_nothing. Then record_knowledge_usage.',
6
- 'Terminal-safe Markdown. A match is a candidate only. If it supports the answer, MUST begin with sourceMarker.leadMarkdown and append sourceMarker.markdown; otherwise use noMatchSourceMarker (empty). Keep unchanged; never wrap in HTML.'
2
+ 'Each user task: hook-provided task context via begin_task_context; otherwise before other tools/answer call exactly get_collaboration_preferences(projectPath=cwd taskPrompt=current user request) as fallback; never substitute a project action. Apply all effective_preferences: personal-global everywhere; matched project, authorized inheritable parent. get_user_taste_skill never replaces a user-authored Skill. Resolve deferred_conflict; never guess personalProjectId. Writes use actual payload; final text not compliance.',
3
+ 'Before asking for stable project fact or method inspect task_knowledge_recall. On miss search_current_project_knowledge with focused action, artifact, target-system, or identifier queries; never use the whole conversational request as the only query. Search active child first, then authorized inheritable parent (PART_OF/USES_KNOWLEDGE_FROM); local_only/RELATED_TO never inherit.',
4
+ 'Before saying unknown call search_knowledge_graph; exact IDs for extra projects. RELATED_TO suggests; ask. all_local_confirmed only after consent; rg safe current repo/workspace; Never scan outside it. monitoring/Git MCP; search_connected_knowledge; agent_decide response-only.',
5
+ 'record_decision_trace; failures record_knowledge_feedback. Batch durable confirmed knowledge: capture_session_knowledge; no secrets. checkpoint_task_knowledge: capture_candidates or retain_nothing; record_knowledge_usage.',
6
+ 'Team: coordinate_project_agent_task gives isolated contexts; Fuli never spawns. Host: acquire_runtime_lease, start workers, report activity/executor, release_runtime_lease in finally.',
7
+ 'Non-empty Provider executionSummary: one row/worker: occupation emoji/label, actual executor/sourceApplication, work summary, workerStatus/status. Empty/absent executionSummary: omit. Configured/allowed/available are not evidence.',
8
+ 'Terminal-safe Markdown; never wrap in HTML. A match is a candidate only. If it supports the answer, MUST begin sourceMarker.leadMarkdown and append sourceMarker.markdown; otherwise use noMatchSourceMarker (empty). Keep unchanged.'
7
9
  ].join(' ');