fuli-context 0.7.6 → 0.7.8

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 (150) hide show
  1. package/README.md +69 -13
  2. package/README.zh-CN.md +60 -14
  3. package/dist/web/assets/{AboutPage-sLV9WChi.js → AboutPage-9CVX91tr.js} +1 -1
  4. package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-CWbCVFdv.js} +1 -1
  5. package/dist/web/assets/EmployeeRecruitDialog-CQgNpGJ_.js +1 -0
  6. package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
  7. package/dist/web/assets/EmployeeWorkbenchPage-CWg4WJhr.css +1 -0
  8. package/dist/web/assets/EmployeeWorkbenchPage-CwU4qBgD.js +1 -0
  9. package/dist/web/assets/KnowledgeEditDialog-B6TGAcFH.js +1 -0
  10. package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
  11. package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-M2pLY0bR.js} +1 -1
  12. package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-DzeAlk9b.js} +1 -1
  13. package/dist/web/assets/KnowledgePage-BJu9w0fF.js +1 -0
  14. package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CCt8rnBb.js} +1 -1
  15. package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-CZswVqui.js} +1 -1
  16. package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-f1nSF6kx.js} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-D__eJf0t.js +1 -0
  18. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
  19. package/dist/web/assets/ProjectAgentsPage-LrwagMGj.js +11 -0
  20. package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BdC2Qk4n.js} +1 -1
  21. package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-Cbgl8_Si.js} +1 -1
  22. package/dist/web/assets/SearchableSelect-ERm-k4KI.js +1 -0
  23. package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-DWso4x29.js} +1 -1
  24. package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-CFpCjmIY.js} +1 -1
  25. package/dist/web/assets/catalog-WGLn7ERJ.js +1 -0
  26. package/dist/web/assets/{console-BT1P2Db4.js → console-DQgu0JIA.js} +1 -1
  27. package/dist/web/assets/conversation-launcher-settings-0KtlG_LV.js +1 -0
  28. package/dist/web/assets/{i18n-js6gfc7j.js → i18n-ByRiA3pJ.js} +3 -3
  29. package/dist/web/assets/{identity-CfDfanpa.js → identity-CxdPRMwH.js} +1 -1
  30. package/dist/web/assets/index-JAZe6lfX.js +2 -0
  31. package/dist/web/assets/index-wcXzzggp.css +1 -0
  32. package/dist/web/assets/{model-DJkLAJsg.js → model-Db4-KefQ.js} +1 -1
  33. package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-CePMjc08.js} +1 -1
  34. package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzX-hn3P.js} +1 -1
  35. package/dist/web/index.html +7 -6
  36. package/docs/employee-agents.md +99 -0
  37. package/docs/project-agent-memory.md +479 -0
  38. package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
  39. package/graph-provider/fuli_graph/model_base.py +5 -0
  40. package/graph-provider/fuli_graph/model_validation.py +32 -0
  41. package/graph-provider/fuli_graph/models.py +26 -170
  42. package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
  43. package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
  44. package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
  45. package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
  46. package/graph-provider/fuli_graph/project_agent_models.py +9 -0
  47. package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
  48. package/graph-provider/fuli_graph/project_agent_routes.py +21 -1
  49. package/graph-provider/fuli_graph/project_agent_task_models.py +217 -29
  50. package/graph-provider/fuli_graph/runtime.py +37 -1
  51. package/graph-provider/fuli_graph/store.py +6 -0
  52. package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
  53. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
  54. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
  55. package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
  56. package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
  57. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
  58. package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
  59. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
  60. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +748 -162
  61. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
  62. package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
  63. package/graph-provider/fuli_graph/store_project_agents.py +214 -46
  64. package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
  65. package/graph-provider/fuli_graph/store_transactions.py +40 -0
  66. package/graph-provider/fuli_graph/task_context_models.py +50 -0
  67. package/graph-provider/fuli_graph/task_context_routes.py +31 -0
  68. package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
  69. package/graph-provider/pyproject.toml +1 -1
  70. package/npm-shrinkwrap.json +8 -8
  71. package/package.json +5 -2
  72. package/skills/capturing-session-knowledge/SKILL.md +18 -6
  73. package/src/adaptive-runtime/executor-pool.js +46 -5
  74. package/src/adaptive-runtime/graph-services.js +10 -3
  75. package/src/adaptive-runtime/lease-client.js +66 -14
  76. package/src/adaptive-runtime/runtime-broker.js +24 -9
  77. package/src/agent-tools/employee-definitions.js +43 -0
  78. package/src/agent-tools/graph-definitions.js +25 -6
  79. package/src/agent-tools/graph-handlers.js +11 -0
  80. package/src/agent-tools/project-agent-definitions.js +35 -4
  81. package/src/agent-tools/project-agent-memory-definitions.js +36 -0
  82. package/src/agent-tools.js +6 -2
  83. package/src/agents/codex/lifecycle-hook.js +107 -0
  84. package/src/agents/codex/lifecycle-hooks.js +231 -0
  85. package/src/agents/cursor/lifecycle-hook.js +133 -0
  86. package/src/agents/cursor/lifecycle-hooks.js +50 -0
  87. package/src/agents/lifecycle-message.js +19 -0
  88. package/src/app/agent-request-context.js +23 -0
  89. package/src/cli/command-registry.js +2 -0
  90. package/src/cli/employee-command.js +17 -0
  91. package/src/cli/remote-mcp-command.js +282 -0
  92. package/src/cli/setup-command.js +5 -0
  93. package/src/cli/update-command.js +6 -43
  94. package/src/cli.js +10 -0
  95. package/src/employees/catalog/jefa.json +21 -0
  96. package/src/employees/install-package.js +77 -0
  97. package/src/employees/management-policy.js +88 -0
  98. package/src/employees/manifest.js +53 -0
  99. package/src/employees/package-registry.js +107 -0
  100. package/src/employees/project-selection.js +50 -0
  101. package/src/employees/recruitment.js +199 -0
  102. package/src/employees/service.js +83 -0
  103. package/src/employees/task-entry.js +39 -0
  104. package/src/graphiti/agent-knowledge-workflows.js +135 -25
  105. package/src/graphiti/federated-application.js +78 -35
  106. package/src/graphiti/project-agent-control-plane.js +20 -3
  107. package/src/graphiti/project-agent-mapping.js +155 -5
  108. package/src/graphiti/project-agent-memory.js +115 -0
  109. package/src/graphiti/project-agent-task-entry.js +126 -0
  110. package/src/graphiti/project-agent-workflows.js +30 -14
  111. package/src/graphiti/provider-client.js +66 -4
  112. package/src/http/api-router.js +30 -5
  113. package/src/http/employee-api-router.js +64 -0
  114. package/src/http/error-mapping.js +4 -0
  115. package/src/http/graph-api-router.js +2 -0
  116. package/src/http/request-policy.js +6 -3
  117. package/src/http/static-handler.js +4 -3
  118. package/src/mcp/create-mcp-server.js +98 -37
  119. package/src/mcp/direct-call.js +38 -0
  120. package/src/mcp/instructions.js +8 -7
  121. package/src/mcp/provider-task-context-registry.js +74 -0
  122. package/src/mcp/remote-http-server.js +761 -0
  123. package/src/mcp/session-id.js +29 -3
  124. package/src/mcp/task-context-registry.js +88 -29
  125. package/src/mcp/tool-annotations.js +10 -2
  126. package/src/mcp/tool-result.js +8 -3
  127. package/src/mcp-server.js +20 -8
  128. package/src/native-runtime/runtime.js +58 -8
  129. package/src/semantic-version.js +42 -0
  130. package/src/server.js +76 -28
  131. package/src/setup/agent-installation-status.js +38 -6
  132. package/src/setup/agents.js +20 -8
  133. package/src/setup/claude-code-config.js +55 -13
  134. package/src/setup/codex-bootstrap.js +11 -10
  135. package/src/setup/codex-config.js +74 -7
  136. package/src/setup/config-backup.js +18 -2
  137. package/src/setup/cursor-config.js +22 -5
  138. package/src/setup/setup.js +30 -22
  139. package/src/storage/json-file.js +11 -3
  140. package/src/system/package-version-checker.js +72 -0
  141. package/src/system/system-service.js +3 -0
  142. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
  143. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
  144. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
  145. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
  146. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
  147. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
  148. package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
  149. package/dist/web/assets/index-BnH5mcZx.css +0 -1
  150. package/dist/web/assets/index-GqJf1xqg.js +0 -2
@@ -1,4 +1,5 @@
1
1
  import { ApplicationError } from '../app/application-error.js';
2
+ import { activeAgentRequestSignal } from '../app/agent-request-context.js';
2
3
 
3
4
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
4
5
  const MAX_REQUEST_TIMEOUT_MS = 2_147_483_647;
@@ -102,6 +103,35 @@ export class GraphitiProviderClient {
102
103
  `/v1/project-agents/${encodeURIComponent(agentId)}?${query}`
103
104
  );
104
105
  }
106
+ getProjectAgentMemory({ personalSpaceId, personalProjectId, agentId, limit = 1 }) {
107
+ const query = new URLSearchParams({
108
+ personal_space_id: personalSpaceId, personal_project_id: personalProjectId,
109
+ limit: String(limit)
110
+ });
111
+ return this.#request(`/v1/project-agents/${encodeURIComponent(agentId)}/memory?${query}`);
112
+ }
113
+ writeProjectAgentMemory(input) {
114
+ return this.#request(`/v1/project-agents/${encodeURIComponent(input.agent_id)}/memory`, {
115
+ method: 'PUT', body: input
116
+ });
117
+ }
118
+ resolveProjectAgentContext(input) {
119
+ return this.#request('/v1/project-agent-context/resolve', { method: 'POST', body: input });
120
+ }
121
+ beginTaskContext(input) {
122
+ return this.#request('/v1/task-contexts', { method: 'PUT', body: input });
123
+ }
124
+ getTaskContext(token, query) {
125
+ return this.#request(`/v1/task-contexts/${encodeURIComponent(token)}?${new URLSearchParams(query)}`);
126
+ }
127
+ checkpointTaskContext(token, input) {
128
+ return this.#request(`/v1/task-contexts/${encodeURIComponent(token)}/checkpoint`, {
129
+ method: 'PUT', body: input
130
+ });
131
+ }
132
+ verifyTaskCheckpoint(query) {
133
+ return this.#request(`/v1/task-context-sessions/checkpoint?${new URLSearchParams(query)}`);
134
+ }
105
135
  deleteProjectAgent(personalSpaceId, agentId, reason = 'archived by user') {
106
136
  const query = new URLSearchParams({
107
137
  personal_space_id: personalSpaceId,
@@ -169,8 +199,12 @@ export class GraphitiProviderClient {
169
199
  if (limit !== null && limit !== undefined) query.set('limit', String(limit));
170
200
  return this.#request(`/v1/project-agent-tasks?${query}`);
171
201
  }
172
- viewProjectAgentTask(personalSpaceId, taskId, { includeEvents = true } = {}) {
202
+ viewProjectAgentTask(personalSpaceId, taskId, {
203
+ personalProjectId = null,
204
+ includeEvents = true
205
+ } = {}) {
173
206
  const query = new URLSearchParams({ personal_space_id: personalSpaceId });
207
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
174
208
  query.set('include_events', String(includeEvents));
175
209
  return this.#request(
176
210
  `/v1/project-agent-tasks/${encodeURIComponent(taskId)}?${query}`
@@ -186,6 +220,7 @@ export class GraphitiProviderClient {
186
220
  }
187
221
  listProjectAgentActivity({
188
222
  personalSpaceId,
223
+ personalProjectId = null,
189
224
  agentId,
190
225
  fromDate = null,
191
226
  toDate = null
@@ -196,6 +231,7 @@ export class GraphitiProviderClient {
196
231
  from: fromDate ?? toDate ?? today,
197
232
  to: toDate ?? fromDate ?? today
198
233
  });
234
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
199
235
  return this.#request(
200
236
  `/v1/project-agents/${encodeURIComponent(agentId)}/activity?${query}`
201
237
  );
@@ -634,7 +670,10 @@ export class GraphitiProviderClient {
634
670
  timedOut = true;
635
671
  controller.abort(new Error('Graphiti provider request timed out'));
636
672
  }, this.requestTimeoutMs);
637
- const { signal } = controller;
673
+ const callerSignal = activeAgentRequestSignal();
674
+ const signal = callerSignal
675
+ ? AbortSignal.any([controller.signal, callerSignal])
676
+ : controller.signal;
638
677
  let response;
639
678
  let payload;
640
679
  try {
@@ -646,6 +685,10 @@ export class GraphitiProviderClient {
646
685
  });
647
686
  payload = await parseResponse(response);
648
687
  } catch (error) {
688
+ if (error instanceof ProviderRequestError) throw error;
689
+ if (!timedOut && callerSignal?.aborted) {
690
+ throw callerSignal.reason ?? new DOMException('Aborted', 'AbortError');
691
+ }
649
692
  const diagnostic = {
650
693
  category: timedOut ? 'provider_timeout' : 'provider_unavailable',
651
694
  status: timedOut ? 504 : 0,
@@ -684,9 +727,28 @@ export class GraphitiProviderClient {
684
727
 
685
728
  async function parseResponse(response) {
686
729
  const type = response.headers.get('content-type') ?? '';
687
- if (type.includes('application/json')) return response.json();
688
730
  const text = await response.text();
689
- return text ? { detail: text } : null;
731
+ if (!type.includes('application/json')) {
732
+ if (!response.ok) return text ? { detail: text } : null;
733
+ throw invalidResponse(
734
+ text ? 'Graphiti provider returned a non-JSON response.'
735
+ : 'Graphiti provider returned an empty response.'
736
+ );
737
+ }
738
+ if (!text.trim()) {
739
+ if (!response.ok) return null;
740
+ throw invalidResponse('Graphiti provider returned an empty response.');
741
+ }
742
+ try {
743
+ return JSON.parse(text);
744
+ } catch {
745
+ if (!response.ok) return null;
746
+ throw invalidResponse('Graphiti provider returned invalid JSON.');
747
+ }
748
+ }
749
+
750
+ function invalidResponse(message) {
751
+ return new ProviderRequestError(message, { code: 'provider_invalid_response' });
690
752
  }
691
753
 
692
754
  function providerErrorDiagnostic(status, payload) {
@@ -1,6 +1,7 @@
1
1
  import { readJson, sendJson } from './response.js';
2
2
  import { handleExternalKnowledgeApiRequest } from './external-knowledge-api-router.js';
3
3
  import { handleGraphApiRequest } from './graph-api-router.js';
4
+ import { handleEmployeeApiRequest } from './employee-api-router.js';
4
5
 
5
6
  export async function handleApiRequest({
6
7
  request,
@@ -36,13 +37,24 @@ export async function handleApiRequest({
36
37
  return true;
37
38
  }
38
39
 
40
+ if (system && url.pathname === '/api/system/version' && request.method === 'GET') {
41
+ sendJson(response, 200, await system.versionStatus());
42
+ return true;
43
+ }
44
+
39
45
  if (system && url.pathname === '/api/system/runtime' && request.method === 'GET') {
40
46
  sendJson(response, 200, system.runtimeStatus());
41
47
  return true;
42
48
  }
43
49
 
44
50
  if (system && url.pathname === '/api/system/runtime/leases' && request.method === 'POST') {
45
- sendJson(response, 201, await system.acquireRuntimeLease(await readJson(request)));
51
+ const lease = await system.acquireRuntimeLease(await readJson(request));
52
+ if (response.destroyed) {
53
+ // Startup can outlive the caller's timeout. No recipient can now renew/release this handle.
54
+ if (lease.leaseId) system.releaseRuntimeLease(lease.leaseId);
55
+ return true;
56
+ }
57
+ sendJson(response, 201, lease);
46
58
  return true;
47
59
  }
48
60
 
@@ -51,7 +63,7 @@ export async function handleApiRequest({
51
63
  sendJson(
52
64
  response,
53
65
  200,
54
- system.refreshRuntimeLease(decodeURIComponent(runtimeLease[1]))
66
+ system.refreshRuntimeLease(decodeRuntimeLeaseId(runtimeLease[1]))
55
67
  );
56
68
  return true;
57
69
  }
@@ -59,7 +71,7 @@ export async function handleApiRequest({
59
71
  sendJson(
60
72
  response,
61
73
  200,
62
- system.releaseRuntimeLease(decodeURIComponent(runtimeLease[1]))
74
+ system.releaseRuntimeLease(decodeRuntimeLeaseId(runtimeLease[1]))
63
75
  );
64
76
  return true;
65
77
  }
@@ -76,8 +88,12 @@ export async function handleApiRequest({
76
88
  : await externalRequest();
77
89
  if (handledExternal) return true;
78
90
 
79
- const graphRequest = () => handleGraphApiRequest({ request, response, url, app });
80
- if (system?.withGraphRuntimeLease && url.pathname.startsWith('/api/')) {
91
+ const graphRequest = async () =>
92
+ await handleEmployeeApiRequest({ request, response, url, app }) ||
93
+ await handleGraphApiRequest({ request, response, url, app });
94
+ if (system?.withGraphRuntimeLease && (
95
+ url.pathname.startsWith('/api/') || url.pathname.startsWith('/employee-workspaces/')
96
+ )) {
81
97
  return system.withGraphRuntimeLease(
82
98
  `http:${request.method}:${url.pathname}`,
83
99
  graphRequest
@@ -86,6 +102,15 @@ export async function handleApiRequest({
86
102
  return graphRequest();
87
103
  }
88
104
 
105
+ function decodeRuntimeLeaseId(value) {
106
+ try {
107
+ return decodeURIComponent(value);
108
+ } catch (error) {
109
+ if (error instanceof URIError) throw new TypeError('Runtime lease ID is invalid');
110
+ throw error;
111
+ }
112
+ }
113
+
89
114
  function externalRequestNeedsGraph(pathname, method) {
90
115
  if (pathname === '/api/connected-knowledge/search' && method === 'POST') return true;
91
116
  if (pathname === '/api/external-knowledge/bindings' && method === 'POST') return true;
@@ -0,0 +1,64 @@
1
+ import { EmployeeError } from '../employees/manifest.js';
2
+ import { readJson, sendJson } from './response.js';
3
+
4
+ export async function handleEmployeeApiRequest({ request, response, url, app }) {
5
+ const employees = app.employees;
6
+ const apiPath = url.pathname.match(/^\/api\/employee-templates(?:\/([^/]+)\/(recruit|workspace|tools|call))?$/);
7
+ const workspacePath = url.pathname.match(/^\/employee-workspaces\/([^/]+)\/([^/]+)(\/.*)?$/);
8
+ if (!apiPath && !workspacePath) return false;
9
+ if (!employees) throw new EmployeeError('Employee runtime is unavailable', 503, 'runtime_unavailable');
10
+ if (workspacePath) {
11
+ const templateId = decodeEmployeePathSegment(workspacePath[1]);
12
+ const personalProjectId = decodeEmployeePathSegment(workspacePath[2]);
13
+ if (!workspacePath[3]) {
14
+ response.writeHead(308, { location: `${url.pathname}/${url.search}` });
15
+ response.end();
16
+ return true;
17
+ }
18
+ await employees.handleHttp(request, response, {
19
+ templateId,
20
+ personalProjectId,
21
+ relativePath: `${workspacePath[3]}${url.search}`,
22
+ origin: `http://${request.headers.host}`
23
+ });
24
+ return true;
25
+ }
26
+ const input = {
27
+ templateId: apiPath[1] ? decodeEmployeePathSegment(apiPath[1]) : undefined,
28
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
29
+ personalProjectId: url.searchParams.get('personalProjectId')
30
+ };
31
+ let result;
32
+ if (!apiPath[1] && request.method === 'GET') result = await employees.list(input);
33
+ else if (apiPath[2] === 'recruit' && request.method === 'POST') {
34
+ const body = await readJson(request);
35
+ if (!body || typeof body !== 'object' || Array.isArray(body) ||
36
+ Object.keys(body).some((key) => !['personalSpaceId', 'personalProjectId', 'personalProjectIds', 'replaceAssignments', 'expectedAssignmentsVersion', 'reactivate', 'management'].includes(key)) ||
37
+ (body.reactivate !== undefined && typeof body.reactivate !== 'boolean')) {
38
+ throw new TypeError('Invalid employee recruitment request');
39
+ }
40
+ result = await employees.recruit({ ...body, templateId: input.templateId });
41
+ } else if (apiPath[2] === 'workspace' && request.method === 'GET') result = await employees.workspace(input);
42
+ else if (apiPath[2] === 'tools' && request.method === 'GET') result = await employees.describeTools(input);
43
+ else if (apiPath[2] === 'call' && request.method === 'POST') {
44
+ const body = await readJson(request);
45
+ if (!body || typeof body !== 'object' || Array.isArray(body) ||
46
+ Object.keys(body).some((key) => !['personalSpaceId', 'personalProjectId', 'tool', 'arguments'].includes(key)) ||
47
+ typeof body.tool !== 'string') throw new TypeError('Invalid employee tool call');
48
+ result = await employees.callTool({ ...body, templateId: input.templateId });
49
+ } else {
50
+ sendJson(response, 405, { error: 'Method not allowed' });
51
+ return true;
52
+ }
53
+ sendJson(response, 200, result);
54
+ return true;
55
+ }
56
+
57
+ function decodeEmployeePathSegment(value) {
58
+ try {
59
+ return decodeURIComponent(value);
60
+ } catch (error) {
61
+ if (error instanceof URIError) throw new TypeError('Invalid employee path encoding');
62
+ throw error;
63
+ }
64
+ }
@@ -4,6 +4,7 @@ import {
4
4
  } from '../app/application-error.js';
5
5
  import { JsonBodyTooLargeError } from './response.js';
6
6
  import { ProviderRequestError } from '../graphiti/provider-client.js';
7
+ import { EmployeeError } from '../employees/manifest.js';
7
8
 
8
9
  const BAD_REQUEST_CODES = new Set([
9
10
  ApplicationErrorCode.NOT_FOUND,
@@ -11,6 +12,9 @@ const BAD_REQUEST_CODES = new Set([
11
12
  ]);
12
13
 
13
14
  export function mapHttpError(error) {
15
+ if (error instanceof EmployeeError) {
16
+ return { status: error.status, body: { error: error.message, code: error.code } };
17
+ }
14
18
  if (error instanceof JsonBodyTooLargeError) {
15
19
  return { status: 413, body: { error: 'Request body too large' } };
16
20
  }
@@ -265,6 +265,7 @@ export async function handleGraphApiRequest({
265
265
  if (projectAgentTaskPath && request.method === 'GET') {
266
266
  sendJson(response, 200, await app.viewProjectAgentTask({
267
267
  personalSpaceId: url.searchParams.get('personalSpaceId'),
268
+ personalProjectId: url.searchParams.get('personalProjectId'),
268
269
  taskId: decodeURIComponent(projectAgentTaskPath[1]),
269
270
  includeEvents: url.searchParams.get('includeEvents') !== 'false'
270
271
  }));
@@ -273,6 +274,7 @@ export async function handleGraphApiRequest({
273
274
  if (url.pathname === '/api/project-agent-activity' && request.method === 'GET') {
274
275
  sendJson(response, 200, await app.viewProjectAgentActivity({
275
276
  personalSpaceId: url.searchParams.get('personalSpaceId'),
277
+ personalProjectId: url.searchParams.get('personalProjectId'),
276
278
  agentId: url.searchParams.get('agentId'),
277
279
  fromDate: url.searchParams.get('fromDate'),
278
280
  toDate: url.searchParams.get('toDate')
@@ -54,10 +54,13 @@ export function rejectRequestOutsidePolicy({
54
54
  function hasLanAccess(authorization, accessToken) {
55
55
  if (typeof accessToken !== 'string' || !accessToken) return false;
56
56
  const expected = `Basic ${Buffer.from(`fuli:${accessToken}`).toString('base64')}`;
57
- if (typeof authorization !== 'string' || authorization.length !== expected.length) {
57
+ if (typeof authorization !== 'string') {
58
58
  return false;
59
59
  }
60
- return timingSafeEqual(Buffer.from(authorization), Buffer.from(expected));
60
+ const providedBytes = Buffer.from(authorization);
61
+ const expectedBytes = Buffer.from(expected);
62
+ return providedBytes.length === expectedBytes.length &&
63
+ timingSafeEqual(providedBytes, expectedBytes);
61
64
  }
62
65
 
63
66
  function isLoopbackAddress(address) {
@@ -67,7 +70,7 @@ function isLoopbackAddress(address) {
67
70
  function requiresJson(request) {
68
71
  if (!['PATCH', 'POST', 'PUT'].includes(request.method)) return false;
69
72
  const pathname = new URL(request.url, 'http://127.0.0.1').pathname;
70
- return pathname.startsWith('/api/');
73
+ return pathname.startsWith('/api/') || pathname.startsWith('/employee-workspaces/');
71
74
  }
72
75
 
73
76
  function isJson(contentType) {
@@ -38,7 +38,7 @@ export function serveStatic(pathname, response) {
38
38
  const pathFromRoot = relative(WEB_ROOT, requestedFile);
39
39
  if (pathFromRoot === '..' || pathFromRoot.startsWith(`..${sep}`) ||
40
40
  isAbsolute(pathFromRoot)) {
41
- response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
41
+ response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
42
42
  response.end('Not found');
43
43
  return;
44
44
  }
@@ -49,13 +49,14 @@ export function serveStatic(pathname, response) {
49
49
  ? join(WEB_ROOT, 'index.html')
50
50
  : null;
51
51
  if (!filePath || !existsSync(filePath)) {
52
- response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
52
+ response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
53
53
  response.end('Not found');
54
54
  return;
55
55
  }
56
56
 
57
57
  response.writeHead(200, {
58
- 'content-type': MIME_TYPES[extname(filePath)] ?? 'application/octet-stream'
58
+ 'content-type': MIME_TYPES[extname(filePath)] ?? 'application/octet-stream',
59
+ ...(extname(filePath) === '.html' ? { 'cache-control': 'no-cache' } : {})
59
60
  });
60
61
  response.end(readFileSync(filePath));
61
62
  }
@@ -1,7 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema
5
+ } from '@modelcontextprotocol/sdk/types.js';
3
6
 
4
7
  import { callAgentTool, listAgentTools } from '../agent-tools.js';
8
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
5
9
  import { FULI_VERSION } from '../package-metadata.js';
6
10
  import { MCP_INSTRUCTIONS } from './instructions.js';
7
11
  import { createProjectActionPreviewTokens } from './project-action-preview-tokens.js';
@@ -24,8 +28,13 @@ import {
24
28
  } from './session-id.js';
25
29
 
26
30
  const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
27
- begin_task_context: 16 * 1024,
28
- get_collaboration_preferences: 16 * 1024,
31
+ list_employee_templates: 32 * 1024,
32
+ recruit_employee: 64 * 1024,
33
+ list_employee_tools: 64 * 1024,
34
+ call_employee_tool: 64 * 1024,
35
+ begin_task_context: 64 * 1024,
36
+ checkpoint_task_knowledge: 16 * 1024,
37
+ get_collaboration_preferences: 64 * 1024,
29
38
  get_user_taste_skill: 32 * 1024,
30
39
  search_knowledge_graph: 32 * 1024,
31
40
  search_connected_knowledge: 64 * 1024,
@@ -38,6 +47,8 @@ const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
38
47
  get_project_agent: 32 * 1024,
39
48
  list_project_agent_assignments: 32 * 1024,
40
49
  get_project_agent_context: 64 * 1024,
50
+ get_project_agent_memory: 128 * 1024,
51
+ checkpoint_project_agent_memory: 64 * 1024,
41
52
  coordinate_project_agent_task: 256 * 1024,
42
53
  acquire_runtime_lease: 4 * 1024,
43
54
  refresh_runtime_lease: 4 * 1024,
@@ -63,33 +74,46 @@ export function createMcpServer(
63
74
  env = process.env,
64
75
  clock = () => new Date(),
65
76
  sourceApplication = nativeCodexThreadId(env) ? 'codex' : 'other',
66
- withRuntimeLease = (_owner, operation) => operation()
77
+ withRuntimeLease = (_owner, operation) => operation(),
78
+ hostSessionId = null,
79
+ instructions = MCP_INSTRUCTIONS,
80
+ toolNames = null,
81
+ prepareToolInput = (_name, input) => input,
82
+ registerResources = true
67
83
  } = {}
68
84
  ) {
69
85
  const server = new McpServer(
70
86
  { name: 'fuli', version: FULI_VERSION },
71
- { instructions: MCP_INSTRUCTIONS }
87
+ { instructions }
72
88
  );
73
89
 
74
- const nativeThreadId = nativeCodexThreadId(env);
75
90
  const authoritativeSourceApplication = normalizeMcpSourceApplication(
76
91
  sourceApplication
77
92
  );
93
+ const nativeThreadId = authoritativeSourceApplication === 'codex' ? nativeCodexThreadId(env) : null;
78
94
  const tools = createToolMap(
79
95
  app,
80
96
  createProjectActionPreviewTokens(),
81
97
  nativeThreadId,
82
- mcpHostSessionId(env),
98
+ hostSessionId ?? mcpHostSessionId(authoritativeSourceApplication === 'codex' ? env : {}),
83
99
  clock,
84
100
  authoritativeSourceApplication,
85
- withRuntimeLease
101
+ withRuntimeLease,
102
+ toolNames,
103
+ prepareToolInput
86
104
  );
87
105
  for (const tool of tools.values()) registerTool(server, tool);
88
- registerFuliContextResources(server, app, { withRuntimeLease });
106
+ if (!tools.size) registerEmptyToolList(server);
107
+ if (registerResources) registerFuliContextResources(server, app, { withRuntimeLease });
89
108
  registerCallHandler(server, tools);
90
109
  return server;
91
110
  }
92
111
 
112
+ function registerEmptyToolList(server) {
113
+ server.server.registerCapabilities({ tools: { listChanged: true } });
114
+ server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [] }));
115
+ }
116
+
93
117
  function createToolMap(
94
118
  app,
95
119
  projectActionPreviews,
@@ -97,29 +121,52 @@ function createToolMap(
97
121
  hostSessionId,
98
122
  clock,
99
123
  sourceApplication,
100
- withRuntimeLease
124
+ withRuntimeLease,
125
+ toolNames,
126
+ prepareToolInput
101
127
  ) {
102
128
  const commonKnowledgePreviews = createCommonKnowledgePreviewTokens();
103
- return new Map(listAgentTools().map((definition) => [definition.name, {
129
+ if (toolNames !== null && !Array.isArray(toolNames)) {
130
+ throw new TypeError('MCP toolNames must be an array or null');
131
+ }
132
+ const definitions = listAgentTools();
133
+ const allowedTools = toolNames === null ? null : new Set(toolNames);
134
+ if (allowedTools) {
135
+ const available = new Set(definitions.map(({ name }) => name));
136
+ const unknown = [...allowedTools].filter((name) => !available.has(name));
137
+ if (unknown.length) {
138
+ throw new TypeError(
139
+ `Unknown MCP tool name${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`
140
+ );
141
+ }
142
+ }
143
+ return new Map(definitions
144
+ .filter((definition) => !allowedTools || allowedTools.has(definition.name))
145
+ .map((definition) => [definition.name, {
104
146
  definition,
105
147
  schema: jsonSchemaToZod(definition.inputSchema),
106
- invoke: (input) => withRuntimeLease(
107
- `mcp-tool:${definition.name}`,
108
- () => invokeAgentTool(
109
- app,
110
- definition.name,
111
- normalizeAgentSessionInput(
148
+ invoke: (input, requestContext = null) => {
149
+ requestContext?.signal?.throwIfAborted?.();
150
+ return withRuntimeLease(
151
+ `mcp-tool:${definition.name}`,
152
+ () => invokeAgentTool(
153
+ app,
112
154
  definition.name,
113
- input,
114
- nativeThreadId,
115
- hostSessionId,
116
- clock,
117
- sourceApplication
155
+ normalizeAgentSessionInput(
156
+ definition.name,
157
+ prepareToolInput(definition.name, input),
158
+ nativeThreadId,
159
+ hostSessionId,
160
+ clock,
161
+ sourceApplication
162
+ ),
163
+ projectActionPreviews,
164
+ commonKnowledgePreviews,
165
+ requestContext
118
166
  ),
119
- projectActionPreviews,
120
- commonKnowledgePreviews
121
- )
122
- )
167
+ requestContext
168
+ );
169
+ }
123
170
  }]));
124
171
  }
125
172
 
@@ -131,22 +178,35 @@ function registerTool(server, tool) {
131
178
  inputSchema: schema,
132
179
  outputSchema: openObjectSchema(),
133
180
  annotations: annotationsFor(definition.name)
134
- }, (input) => invokeTool(tool, input));
181
+ }, (input, extra) => invokeTool(tool, input, extra));
135
182
  }
136
183
 
137
184
  function registerCallHandler(server, tools) {
138
- server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
185
+ server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
139
186
  const tool = tools.get(request.params.name);
140
- if (!tool) return errorToolResult(new Error('Unknown tool'));
187
+ if (!tool) {
188
+ return errorToolResult(new ApplicationError(
189
+ ApplicationErrorCode.NOT_FOUND,
190
+ 'Unknown Fuli tool'
191
+ ));
192
+ }
141
193
  const parsed = await tool.schema.safeParseAsync(request.params.arguments ?? {});
142
- if (!parsed.success) return protocolErrorResult('Input validation error');
143
- return invokeTool(tool, parsed.data);
194
+ if (!parsed.success) {
195
+ return protocolErrorResult(
196
+ 'Input validation error',
197
+ parsed.error.issues.map(({ path, message }) => ({
198
+ field: path.length ? path.join('.') : 'arguments',
199
+ message
200
+ }))
201
+ );
202
+ }
203
+ return invokeTool(tool, parsed.data, extra);
144
204
  });
145
205
  }
146
206
 
147
- async function invokeTool(tool, input) {
207
+ async function invokeTool(tool, input, requestContext = null) {
148
208
  try {
149
- const value = await tool.invoke(input);
209
+ const value = await tool.invoke(input, requestContext);
150
210
  auditLifecycleTool(tool.definition.name);
151
211
  const limitBytes = TOOL_RESULT_LIMIT_BYTES[tool.definition.name];
152
212
  if (tool.definition.name === 'begin_task_context') {
@@ -167,10 +227,11 @@ async function invokeAgentTool(
167
227
  name,
168
228
  input,
169
229
  projectActionPreviews,
170
- commonKnowledgePreviews
230
+ commonKnowledgePreviews,
231
+ requestContext = null
171
232
  ) {
172
233
  if (name === 'preview_personal_project_action') {
173
- const result = await callAgentTool(app, name, input);
234
+ const result = await callAgentTool(app, name, input, requestContext);
174
235
  return {
175
236
  ...result,
176
237
  previewToken: projectActionPreviews.issue(input)
@@ -180,7 +241,7 @@ async function invokeAgentTool(
180
241
  projectActionPreviews.consume(input.previewToken, input);
181
242
  }
182
243
  if (name === 'preview_common_knowledge_promotion') {
183
- const result = await callAgentTool(app, name, input);
244
+ const result = await callAgentTool(app, name, input, requestContext);
184
245
  return {
185
246
  ...result,
186
247
  previewToken: commonKnowledgePreviews.issue(input)
@@ -189,5 +250,5 @@ async function invokeAgentTool(
189
250
  if (name === 'apply_common_knowledge_promotion') {
190
251
  commonKnowledgePreviews.consume(input.previewToken, input);
191
252
  }
192
- return callAgentTool(app, name, input);
253
+ return callAgentTool(app, name, input, requestContext);
193
254
  }
@@ -0,0 +1,38 @@
1
+ import { listAgentTools } from '../agent-tools.js';
2
+ import {
3
+ mcpHostSessionId,
4
+ nativeCodexThreadId,
5
+ normalizeAgentSessionInput,
6
+ normalizeMcpSourceApplication
7
+ } from './session-id.js';
8
+ import { jsonSchemaToZod } from './tool-schema.js';
9
+
10
+ export async function prepareDirectAgentToolCall({
11
+ toolName,
12
+ input,
13
+ sourceApplication = 'other',
14
+ env = process.env,
15
+ clock = () => new Date(),
16
+ hostSessionId = null
17
+ }) {
18
+ const definition = listAgentTools().find((item) => item.name === toolName);
19
+ if (!definition) throw new TypeError('Unknown tool');
20
+ const parsed = await jsonSchemaToZod(definition.inputSchema).safeParseAsync(input ?? {});
21
+ if (!parsed.success) throw new TypeError('Input validation error');
22
+ const source = normalizeMcpSourceApplication(sourceApplication);
23
+ const nativeThreadId = source === 'codex' ? nativeCodexThreadId(env) : null;
24
+ const authoritativeHostSessionId = hostSessionId ?? mcpHostSessionId(
25
+ source === 'codex' ? env : {}
26
+ );
27
+ return {
28
+ definition,
29
+ input: normalizeAgentSessionInput(
30
+ toolName,
31
+ parsed.data,
32
+ nativeThreadId,
33
+ authoritativeHostSessionId,
34
+ clock,
35
+ source
36
+ )
37
+ };
38
+ }
@@ -1,9 +1,10 @@
1
1
  export const MCP_INSTRUCTIONS = [
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.'
2
+ "Each user task: begin_task_context hook-provided task context; else before tools/answer call exactly get_collaboration_preferences(projectPath=cwd taskPrompt=current user request); never substitute project action. All effective_preferences: personal-global everywhere; matched project, authorized inheritable parent. get_user_taste_skill never replaces user Skill. Resolve deferred_conflict; never guess personalProjectId. Writes=actual payload; final text=not compliance.",
3
+ "Before asking stable project fact/method: task_knowledge_recall; miss: search_current_project_knowledge with focused action/artifact/target/ID queries, never only the full request. Active child first, then inheritable parent; no local_only/RELATED_TO.",
4
+ "Before saying unknown: search_knowledge_graph; extra projects=exact IDs. all_local_confirmed only after consent; rg only current repo/workspace. monitoring/Git MCP; search_connected_knowledge; agent_decide response-only.",
5
+ "record_decision_trace; errors: record_knowledge_feedback. Batch durable confirmed knowledge: capture_session_knowledge; no secrets. checkpoint_task_knowledge: capture_candidates/retain_nothing; record_knowledge_usage.",
6
+ "coordinate_project_agent_task=isolated context; Fuli never spawns. acquire_runtime_lease; run/report; release_runtime_lease in finally.",
7
+ "project_management_context: actionable work via authorized manager+board; keep chosen specialist; no excluded projects/extra model. Rename: exact current native session, host rename tool, manual-title protection; receipt after actual client result.",
8
+ "Task runs until all workerStatus terminal. executionSummary: one table row/worker: occupation emoji/label, actual executor/sourceApplication, work/status/session/link, source-labelled cumulative tokens. Missing=unknown; no invent/estimate/copy totals. Empty: omit. Configured/allowed/available not evidence.",
9
+ "Terminal-safe Markdown; no HTML. Match=candidate only. If supported: MUST begin sourceMarker.leadMarkdown; append sourceMarker.markdown; otherwise noMatchSourceMarker (empty). Keep unchanged."
9
10
  ].join(' ');