fuli-context 0.7.6 → 0.7.7

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 (146) 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-BcZeFcHe.js} +1 -1
  4. package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-xAQYl1SQ.js} +1 -1
  5. package/dist/web/assets/EmployeeRecruitDialog-CbPAmhy7.js +1 -0
  6. package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
  7. package/dist/web/assets/EmployeeWorkbenchPage-BQvTLoc8.css +1 -0
  8. package/dist/web/assets/EmployeeWorkbenchPage-DF3EL0Ui.js +1 -0
  9. package/dist/web/assets/KnowledgeEditDialog-UnnBDt0I.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-BqPJH5Rr.js} +1 -1
  12. package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
  13. package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
  14. package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
  15. package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-Xyfm5xkZ.js} +1 -1
  16. package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-DB9mByB3.js} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-_-YXVi5O.js +1 -0
  18. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
  19. package/dist/web/assets/ProjectAgentsPage-DeeQByPX.js +11 -0
  20. package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
  21. package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-CQYGwFBH.js} +1 -1
  22. package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
  23. package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-BlEZ03Yi.js} +1 -1
  24. package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-DvRt_nmP.js} +1 -1
  25. package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
  26. package/dist/web/assets/{console-BT1P2Db4.js → console-tOxOl8Vq.js} +1 -1
  27. package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
  28. package/dist/web/assets/{i18n-js6gfc7j.js → i18n-CgBHZthI.js} +3 -3
  29. package/dist/web/assets/{identity-CfDfanpa.js → identity-CFo_Uw1H.js} +1 -1
  30. package/dist/web/assets/index-CJc5ppXH.css +1 -0
  31. package/dist/web/assets/index-yCi3cz2l.js +2 -0
  32. package/dist/web/assets/{model-DJkLAJsg.js → model-BtnVr7fX.js} +1 -1
  33. package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
  34. package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzHm-y-x.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.js +10 -0
  94. package/src/employees/catalog/jefa.json +21 -0
  95. package/src/employees/install-package.js +77 -0
  96. package/src/employees/management-policy.js +88 -0
  97. package/src/employees/manifest.js +53 -0
  98. package/src/employees/package-registry.js +107 -0
  99. package/src/employees/project-selection.js +50 -0
  100. package/src/employees/recruitment.js +199 -0
  101. package/src/employees/service.js +83 -0
  102. package/src/employees/task-entry.js +39 -0
  103. package/src/graphiti/agent-knowledge-workflows.js +135 -25
  104. package/src/graphiti/federated-application.js +78 -35
  105. package/src/graphiti/project-agent-control-plane.js +20 -3
  106. package/src/graphiti/project-agent-mapping.js +155 -5
  107. package/src/graphiti/project-agent-memory.js +115 -0
  108. package/src/graphiti/project-agent-task-entry.js +126 -0
  109. package/src/graphiti/project-agent-workflows.js +30 -14
  110. package/src/graphiti/provider-client.js +66 -4
  111. package/src/http/api-router.js +25 -5
  112. package/src/http/employee-api-router.js +64 -0
  113. package/src/http/error-mapping.js +4 -0
  114. package/src/http/graph-api-router.js +2 -0
  115. package/src/http/request-policy.js +6 -3
  116. package/src/http/static-handler.js +4 -3
  117. package/src/mcp/create-mcp-server.js +98 -37
  118. package/src/mcp/direct-call.js +38 -0
  119. package/src/mcp/instructions.js +8 -7
  120. package/src/mcp/provider-task-context-registry.js +74 -0
  121. package/src/mcp/remote-http-server.js +761 -0
  122. package/src/mcp/session-id.js +29 -3
  123. package/src/mcp/task-context-registry.js +88 -29
  124. package/src/mcp/tool-annotations.js +10 -2
  125. package/src/mcp/tool-result.js +8 -3
  126. package/src/mcp-server.js +20 -8
  127. package/src/native-runtime/runtime.js +58 -8
  128. package/src/server.js +76 -28
  129. package/src/setup/agent-installation-status.js +38 -6
  130. package/src/setup/agents.js +20 -8
  131. package/src/setup/claude-code-config.js +55 -13
  132. package/src/setup/codex-bootstrap.js +11 -10
  133. package/src/setup/codex-config.js +74 -7
  134. package/src/setup/config-backup.js +18 -2
  135. package/src/setup/cursor-config.js +22 -5
  136. package/src/setup/setup.js +30 -22
  137. package/src/storage/json-file.js +11 -3
  138. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
  139. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
  140. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
  141. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
  142. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
  143. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
  144. package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
  145. package/dist/web/assets/index-BnH5mcZx.css +0 -1
  146. package/dist/web/assets/index-GqJf1xqg.js +0 -2
@@ -3,11 +3,20 @@ import { randomUUID } from 'node:crypto';
3
3
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
4
  const WORKFLOW_OBSERVATION_TOOL = 'record_workflow_transition_observation';
5
5
  const PROJECT_AGENT_SOURCE_TOOLS = new Set([
6
+ 'call_employee_tool',
7
+ 'begin_task_context',
8
+ 'verify_task_checkpoint',
9
+ 'checkpoint_task_knowledge',
10
+ 'get_collaboration_preferences',
11
+ 'capture_session_knowledge',
12
+ 'record_decision_trace',
6
13
  'create_project_agent_assignment',
7
14
  'replace_project_agent_assignment',
8
15
  'coordinate_project_agent_task',
9
16
  'submit_project_agent_task',
10
17
  'get_project_agent_context',
18
+ 'get_project_agent_memory',
19
+ 'checkpoint_project_agent_memory',
11
20
  'record_project_agent_task_activity',
12
21
  'preflight_executor',
13
22
  'report_executor_health',
@@ -15,6 +24,7 @@ const PROJECT_AGENT_SOURCE_TOOLS = new Set([
15
24
  ]);
16
25
 
17
26
  const SESSION_ID_TOOLS = new Set([
27
+ 'get_collaboration_preferences',
18
28
  'begin_task_context',
19
29
  'verify_task_checkpoint',
20
30
  'capture_session_knowledge',
@@ -51,14 +61,26 @@ export function normalizeAgentSessionInput(
51
61
  }
52
62
  const {
53
63
  sessionId: _sessionId,
64
+ session_id: _session_id,
54
65
  idempotencyKey: _idempotencyKey,
66
+ idempotency_key: _idempotency_key,
55
67
  observedAt: _observedAt,
68
+ observed_at: _observed_at,
56
69
  hostSessionId: _hostSessionId,
70
+ host_session_id: _host_session_id,
57
71
  hostObservedAt: _hostObservedAt,
72
+ host_observed_at: _host_observed_at,
73
+ sourceApplication: _sourceApplication,
74
+ source_application: _source_application,
75
+ sourceSessionId: _sourceSessionId,
76
+ source_session_id: _source_session_id,
77
+ sourceSessionVerified: _sourceSessionVerified,
78
+ source_session_verified: _source_session_verified,
58
79
  ...safeInput
59
80
  } = input ?? {};
60
81
  return {
61
82
  ...safeInput,
83
+ sourceApplication: normalizeMcpSourceApplication(sourceApplication),
62
84
  hostSessionId,
63
85
  hostObservedAt: clock().toISOString()
64
86
  };
@@ -73,24 +95,28 @@ export function normalizeAgentSessionInput(
73
95
  source_application: _source_application,
74
96
  sourceSessionId: _sourceSessionId,
75
97
  source_session_id: _source_session_id,
98
+ sourceSessionVerified: _sourceSessionVerified,
99
+ source_session_verified: _source_session_verified,
76
100
  ...safeInput
77
101
  } = input ?? {};
78
102
  return {
79
103
  ...safeInput,
104
+ ...(SESSION_ID_TOOLS.has(name) && nativeThreadId ? { sessionId: nativeThreadId } : {}),
80
105
  sourceApplication: safeSourceApplication,
81
- sourceSessionId: hostSessionId
106
+ sourceSessionId: hostSessionId,
107
+ ...(name === 'call_employee_tool' ? { sourceSessionVerified: Boolean(nativeThreadId && nativeThreadId === hostSessionId) } : {}),
82
108
  };
83
109
  }
84
110
  if (!nativeThreadId || !SESSION_ID_TOOLS.has(name)) return input;
85
111
  if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
86
- if (typeof input.sessionId !== 'string' || input.sessionId.trim() === nativeThreadId) {
112
+ if (typeof input.sessionId !== 'string' || input.sessionId === nativeThreadId) {
87
113
  return input;
88
114
  }
89
115
  return { ...input, sessionId: nativeThreadId };
90
116
  }
91
117
 
92
118
  export function normalizeMcpSourceApplication(value) {
93
- if (!['codex', 'claude_code', 'cursor', 'kiro', 'other'].includes(value)) {
119
+ if (!['codex', 'claude', 'claude_code', 'cursor', 'kiro', 'other'].includes(value)) {
94
120
  throw new TypeError('Unsupported MCP source application');
95
121
  }
96
122
  return value;
@@ -18,59 +18,91 @@ export class TaskContextRegistry {
18
18
  this.tokens = new Map();
19
19
  }
20
20
 
21
- begin({ sessionId, personalProjectId }) {
21
+ begin({ sessionId, turnId = null, personalProjectId, projectAgentId = null,
22
+ sourceApplication = 'other', sourceSessionId = null, memoryRevision = null }) {
22
23
  assertNonEmpty(sessionId, 'sessionId');
23
- const previous = this.sessions.get(sessionId) ?? null;
24
+ const key = sessionKey(sourceApplication, sessionId);
25
+ const previous = this.sessions.get(key) ?? null;
26
+ if (!previous) this.#reserveSessionSlot();
24
27
  if (previous) this.tokens.delete(previous.token);
25
28
 
26
29
  const task = {
27
30
  token: this.tokenFactory(),
28
31
  sessionId,
32
+ turnId,
29
33
  personalProjectId: personalProjectId ?? null,
34
+ projectAgentId,
35
+ sourceApplication,
36
+ sourceSessionId,
37
+ memoryRevision,
38
+ agentMemory: null,
30
39
  checkpoint: null
31
40
  };
32
- this.sessions.delete(sessionId);
33
- this.sessions.set(sessionId, task);
41
+ this.sessions.delete(key);
42
+ this.sessions.set(key, task);
34
43
  this.tokens.set(task.token, task);
35
- this.#evictOldest();
36
44
  return {
37
- ...task,
38
- previousCheckpointMissing: Boolean(previous && !previous.checkpoint)
45
+ ...snapshot(task),
46
+ previousCheckpointMissing: Boolean(previous && (!previous.checkpoint || previous.checkpoint.phase === 'prepare'))
39
47
  };
40
48
  }
41
49
 
42
- checkpoint(token, checkpoint) {
43
- assertNonEmpty(token, 'taskContextToken');
44
- const task = this.tokens.get(token);
45
- if (!task) throw unknownTokenError();
46
- if (task.checkpoint) {
47
- if (sameCheckpoint(task.checkpoint, checkpoint)) return task;
50
+ checkpoint(token, checkpoint, sourceApplication = 'other') {
51
+ const task = this.#task(token, sourceApplication);
52
+ if (task.checkpoint?.phase === 'prepare') {
53
+ if (!sameCheckpointClaim(task.checkpoint, checkpoint)) {
54
+ throw checkpointConflict('Task checkpoint has different input');
55
+ }
56
+ } else if (task.checkpoint) {
57
+ if (sameCheckpoint(task.checkpoint, checkpoint)) return snapshot(task);
58
+ throw checkpointConflict(
59
+ 'Fuli task context has already been checkpointed with a different disposition'
60
+ );
61
+ }
62
+ task.checkpoint = Object.freeze({ ...checkpoint, phase: 'complete' });
63
+ return snapshot(task);
64
+ }
65
+
66
+ prepare(token, checkpoint, sourceApplication = 'other', agentMemory = null) {
67
+ const task = this.#task(token, sourceApplication);
68
+ if (agentMemory !== null && agentMemory !== undefined) {
48
69
  throw new ApplicationError(
49
70
  ApplicationErrorCode.VALIDATION,
50
- 'Fuli task context has already been checkpointed with a different disposition'
71
+ 'The in-memory task context registry cannot atomically persist durable Agent memory'
51
72
  );
52
73
  }
53
- task.checkpoint = Object.freeze({ ...checkpoint });
54
- return task;
74
+ if (task.checkpoint && !sameCheckpointClaim(task.checkpoint, checkpoint)) {
75
+ throw checkpointConflict('Task checkpoint has different input');
76
+ }
77
+ if (!task.checkpoint) task.checkpoint = { ...checkpoint, phase: 'prepare' };
78
+ return snapshot(task);
55
79
  }
56
80
 
57
- context(token) {
81
+ context(token, sourceApplication = 'other') {
82
+ return snapshot(this.#task(token, sourceApplication));
83
+ }
84
+
85
+ #task(token, sourceApplication) {
58
86
  assertNonEmpty(token, 'taskContextToken');
59
87
  const task = this.tokens.get(token);
60
- if (!task) throw unknownTokenError();
88
+ if (!task || task.sourceApplication !== sourceApplication) throw unknownTokenError();
89
+ const key = sessionKey(task.sourceApplication, task.sessionId);
90
+ if (this.sessions.get(key) !== task) throw unknownTokenError();
91
+ this.sessions.delete(key);
92
+ this.sessions.set(key, task);
61
93
  return task;
62
94
  }
63
95
 
64
- verify(sessionId) {
96
+ verify(sessionId, sourceApplication = 'other') {
65
97
  assertNonEmpty(sessionId, 'sessionId');
66
- const task = this.sessions.get(sessionId);
98
+ const task = this.sessions.get(sessionKey(sourceApplication, sessionId));
67
99
  if (!task) {
68
100
  return {
69
101
  status: 'not_started',
70
102
  guidance: 'No Fuli task context was started; do not block the Agent.'
71
103
  };
72
104
  }
73
- if (task.checkpoint) {
105
+ if (task.checkpoint && task.checkpoint.phase !== 'prepare') {
74
106
  return {
75
107
  status: 'checkpointed',
76
108
  disposition: task.checkpoint.disposition
@@ -79,27 +111,54 @@ export class TaskContextRegistry {
79
111
  return {
80
112
  status: 'checkpoint_required',
81
113
  decision: 'block',
82
- reason: 'Before finishing, call checkpoint_task_knowledge once with either a bounded durable candidate batch or retain_nothing. Do not store raw transcripts, guesses, or temporary output.'
114
+ task_context_token: task.token,
115
+ reason: `FULI_CHECKPOINT_REQUIRED: ${task.token} Before finishing, call checkpoint_task_knowledge once with either a bounded durable candidate batch or retain_nothing. Do not store raw transcripts, guesses, or temporary output.`
83
116
  };
84
117
  }
85
118
 
86
- #evictOldest() {
87
- while (this.sessions.size > MAX_ACTIVE_SESSIONS) {
88
- const [sessionId, task] = this.sessions.entries().next().value;
89
- this.sessions.delete(sessionId);
119
+ #reserveSessionSlot() {
120
+ if (this.sessions.size < MAX_ACTIVE_SESSIONS) return;
121
+ for (const [key, task] of this.sessions) {
122
+ if (task.checkpoint?.phase !== 'complete') continue;
123
+ this.sessions.delete(key);
90
124
  this.tokens.delete(task.token);
125
+ return;
91
126
  }
127
+ throw new ApplicationError(
128
+ ApplicationErrorCode.VALIDATION,
129
+ 'Fuli has too many unfinished in-memory task contexts; finish an existing context or restart this fallback MCP process'
130
+ );
92
131
  }
93
132
  }
94
133
 
134
+ function snapshot(task) {
135
+ return structuredClone(task);
136
+ }
137
+
138
+ function sessionKey(sourceApplication, sessionId) {
139
+ return JSON.stringify([sourceApplication, sessionId]);
140
+ }
141
+
95
142
  function assertNonEmpty(value, label) {
96
143
  if (typeof value !== 'string' || !value.trim()) {
97
- throw new TypeError(`${label} must be a nonempty string`);
144
+ throw new ApplicationError(
145
+ ApplicationErrorCode.VALIDATION,
146
+ `${label} must be a nonempty string`
147
+ );
98
148
  }
99
149
  }
100
150
 
101
151
  function sameCheckpoint(left, right) {
102
- return left.disposition === right.disposition
103
- && left.reason === right.reason
152
+ return sameCheckpointClaim(left, right)
104
153
  && left.captureStatus === right.captureStatus;
105
154
  }
155
+
156
+ function sameCheckpointClaim(left, right) {
157
+ return left.fingerprint === right.fingerprint
158
+ && left.disposition === right.disposition
159
+ && left.reason === right.reason;
160
+ }
161
+
162
+ function checkpointConflict(message) {
163
+ return new ApplicationError(ApplicationErrorCode.VALIDATION, message);
164
+ }
@@ -1,4 +1,6 @@
1
1
  const READ_TOOLS = new Set([
2
+ 'list_employee_templates',
3
+ 'list_employee_tools',
2
4
  'verify_task_checkpoint',
3
5
  'search_current_project_knowledge',
4
6
  'discover_common_knowledge_candidates',
@@ -11,6 +13,7 @@ const READ_TOOLS = new Set([
11
13
  'get_project_agent',
12
14
  'list_project_agent_assignments',
13
15
  'get_project_agent_context',
16
+ 'get_project_agent_memory',
14
17
  'view_project_agent_task',
15
18
  'view_project_agent_activity',
16
19
  'get_project_agent_coordination_policy',
@@ -33,8 +36,11 @@ const READ_TOOLS = new Set([
33
36
  ]);
34
37
 
35
38
  const WRITE_TOOLS = new Set([
39
+ 'recruit_employee',
40
+ 'call_employee_tool',
36
41
  'begin_task_context',
37
42
  'checkpoint_task_knowledge',
43
+ 'checkpoint_project_agent_memory',
38
44
  'get_collaboration_preferences',
39
45
  'get_user_taste_skill',
40
46
  'resolve_deferred_preference_conflict',
@@ -93,6 +99,7 @@ const WRITE_TOOLS = new Set([
93
99
  ]);
94
100
 
95
101
  const DESTRUCTIVE_TOOLS = new Set([
102
+ 'recruit_employee',
96
103
  'resolve_deferred_preference_conflict',
97
104
  'apply_common_knowledge_promotion',
98
105
  'review_project_proposal',
@@ -109,7 +116,7 @@ export function annotationsFor(name) {
109
116
  return {
110
117
  readOnlyHint: readOnly,
111
118
  destructiveHint: DESTRUCTIVE_TOOLS.has(name),
112
- idempotentHint: readOnly || name === 'upsert_personal_project' ||
119
+ idempotentHint: readOnly || name === 'recruit_employee' || name === 'upsert_personal_project' ||
113
120
  name === 'upsert_project_agent' ||
114
121
  name === 'delete_project_agent' ||
115
122
  name === 'cleanup_test_project_agents' ||
@@ -131,7 +138,8 @@ export function annotationsFor(name) {
131
138
  name === 'record_knowledge_usage' || name === 'record_decision_trace' ||
132
139
  name === 'record_knowledge_feedback' ||
133
140
  name === 'record_workflow_transition_observation' ||
134
- name === 'checkpoint_task_knowledge',
141
+ name === 'checkpoint_task_knowledge' ||
142
+ name === 'checkpoint_project_agent_memory',
135
143
  openWorldHint: name === 'capture_session_knowledge' ||
136
144
  name === 'coordinate_project_agent_task' ||
137
145
  name === 'submit_project_agent_task' ||
@@ -100,9 +100,11 @@ export function applicationErrorMessage(error) {
100
100
  return boundedMessage(String(error.message));
101
101
  }
102
102
 
103
- export function protocolErrorResult(message) {
103
+ export function protocolErrorResult(message, validationErrors = []) {
104
104
  if (message.includes('Input validation error')) {
105
- return errorToolResult(new ApplicationError('validation', 'Invalid tool arguments'));
105
+ const error = new ApplicationError('validation', 'Invalid tool arguments');
106
+ error.validationErrors = validationErrors;
107
+ return errorToolResult(error);
106
108
  }
107
109
  return errorToolResult(new Error('Protocol tool failure'));
108
110
  }
@@ -117,8 +119,10 @@ function sanitize(value, state, seen = new WeakSet(), depth = 0) {
117
119
  seen.add(value);
118
120
  if (Array.isArray(value)) {
119
121
  if (value.length > RESULT_ITEM_LIMIT) state.truncated = true;
120
- return value.slice(0, RESULT_ITEM_LIMIT)
122
+ const result = value.slice(0, RESULT_ITEM_LIMIT)
121
123
  .map((item) => sanitize(item, state, seen, depth + 1));
124
+ seen.delete(value);
125
+ return result;
122
126
  }
123
127
 
124
128
  const sanitized = {};
@@ -132,6 +136,7 @@ function sanitize(value, state, seen = new WeakSet(), depth = 0) {
132
136
  sanitized[key] = sanitize(item, state, seen, depth + 1);
133
137
  included += 1;
134
138
  }
139
+ seen.delete(value);
135
140
  return sanitized;
136
141
  }
137
142
 
package/src/mcp-server.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  resolveGraphRuntimeOptions
12
12
  } from './graphiti/runtime-config.js';
13
13
  import { runStdio } from './mcp/runtime.js';
14
+ import { prepareDirectAgentToolCall } from './mcp/direct-call.js';
14
15
  import { normalizeMcpSourceApplication } from './mcp/session-id.js';
15
16
  import { applicationErrorMessage } from './mcp/tool-result.js';
16
17
 
@@ -30,13 +31,19 @@ async function main(args) {
30
31
  }
31
32
 
32
33
  const { runtimeConfigPath } = resolveGraphRuntimeOptions(args, process.env);
33
- if (args.includes('--call')) {
34
- await runCall({ args, runtimeConfigPath });
35
- return;
36
- }
37
34
  const configuredSourceApplication = args.includes('--source-application')
38
35
  ? requiredOption(args, '--source-application')
39
36
  : null;
37
+ if (args.includes('--call')) {
38
+ await runCall({
39
+ args,
40
+ runtimeConfigPath,
41
+ sourceApplication: configuredSourceApplication
42
+ ? normalizeMcpSourceApplication(configuredSourceApplication)
43
+ : 'other'
44
+ });
45
+ return;
46
+ }
40
47
  await runStdio({
41
48
  runtimeConfigPath,
42
49
  sourceApplication: configuredSourceApplication
@@ -45,15 +52,20 @@ async function main(args) {
45
52
  });
46
53
  }
47
54
 
48
- async function runCall({ args, runtimeConfigPath }) {
55
+ async function runCall({ args, runtimeConfigPath, sourceApplication }) {
56
+ const toolName = requiredOption(args, '--call');
57
+ const prepared = await prepareDirectAgentToolCall({
58
+ toolName,
59
+ input: parseInput(args),
60
+ sourceApplication
61
+ });
49
62
  const app = openFederatedGraphApplication({ runtimeConfigPath });
50
63
  const leaseClient = createRuntimeLeaseClient({ runtimeConfigPath });
51
64
  bindRuntimeLeaseAgentTools(app, leaseClient);
52
65
  try {
53
- const toolName = requiredOption(args, '--call');
54
66
  const result = await leaseClient.withGraphLease(
55
67
  `cli-call:${toolName}`,
56
- () => callAgentTool(app, toolName, parseInput(args))
68
+ () => callAgentTool(app, toolName, prepared.input)
57
69
  );
58
70
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
59
71
  } finally {
@@ -86,7 +98,7 @@ function startupMessage(error) {
86
98
 
87
99
  function helpText() {
88
100
  return 'Usage: node src/mcp-server.js [--runtime-config <graph-runtime.json>] ' +
89
- '[--source-application <codex|claude_code|cursor|kiro|other>]\n' +
101
+ '[--source-application <codex|claude|claude_code|cursor|kiro|other>]\n' +
90
102
  ' node src/mcp-server.js --tools\n' +
91
103
  ' node src/mcp-server.js --runtime-config <graph-runtime.json> ' +
92
104
  '--call <tool> [--input <json>]\n';
@@ -13,6 +13,7 @@ import {
13
13
  } from 'node:fs';
14
14
  import {
15
15
  mkdir,
16
+ readdir,
16
17
  readFile,
17
18
  rename,
18
19
  rm,
@@ -22,7 +23,7 @@ import {
22
23
  import { Readable } from 'node:stream';
23
24
  import { pipeline } from 'node:stream/promises';
24
25
  import { createConnection } from 'node:net';
25
- import { dirname, join } from 'node:path';
26
+ import { dirname, join, relative } from 'node:path';
26
27
 
27
28
  import { FULI_VERSION } from '../package-metadata.js';
28
29
  import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
@@ -284,12 +285,18 @@ export async function ensureNativeRuntime(input, dependencies = {}) {
284
285
  }
285
286
 
286
287
  const providerPython = join(input.paths.nativeProviderVenvPath, 'bin', 'python');
288
+ const providerSource = join(dirname(input.paths.graphComposePath), 'graph-provider');
289
+ const providerFingerprint = await deps.providerSourceFingerprint(providerSource);
290
+ if (!/^[a-f0-9]{64}$/.test(providerFingerprint)) {
291
+ throw new Error('Native Graph Provider source fingerprint is invalid.');
292
+ }
287
293
  const expected = {
288
294
  version: MANIFEST_VERSION,
289
295
  status: 'ready',
290
296
  mode: 'native',
291
297
  neo4jVersion: NATIVE_NEO4J_VERSION,
292
298
  providerVersion: `${FULI_VERSION}+native.${PROVIDER_RUNTIME_REVISION}`,
299
+ providerFingerprint,
293
300
  javaHome,
294
301
  uvCommand,
295
302
  neo4jHome: input.paths.nativeNeo4jHome,
@@ -307,14 +314,17 @@ export async function ensureNativeRuntime(input, dependencies = {}) {
307
314
  sha256: NATIVE_NEO4J_SHA256
308
315
  });
309
316
  }
310
- if (!deps.pathExists(providerPython) || saved?.providerVersion !== expected.providerVersion) {
317
+ if (
318
+ !deps.pathExists(providerPython) ||
319
+ saved?.providerVersion !== expected.providerVersion ||
320
+ saved?.providerFingerprint !== expected.providerFingerprint
321
+ ) {
311
322
  deps.onProgress(input, 'Installing the native Graph Provider…');
312
323
  await deps.installProvider({
313
324
  uvCommand,
314
325
  venvPath: input.paths.nativeProviderVenvPath,
315
326
  providerPython,
316
- providerSource: join(dirname(dirname(input.paths.graphComposePath)), 'graph-provider'),
317
- packageRoot: dirname(input.paths.graphComposePath)
327
+ providerSource
318
328
  });
319
329
  }
320
330
  if (!neo4jReady(input.paths, deps.pathExists) || !deps.pathExists(providerPython)) {
@@ -356,6 +366,7 @@ function manifestReady(saved, expected, paths, pathExists) {
356
366
  saved.mode === expected.mode &&
357
367
  saved.neo4jVersion === expected.neo4jVersion &&
358
368
  saved.providerVersion === expected.providerVersion &&
369
+ saved.providerFingerprint === expected.providerFingerprint &&
359
370
  saved.javaHome === expected.javaHome &&
360
371
  saved.uvCommand === expected.uvCommand &&
361
372
  saved.neo4jHome === expected.neo4jHome &&
@@ -385,6 +396,7 @@ function nativeDependencies(overrides) {
385
396
  resolveUvCommand,
386
397
  installNeo4j,
387
398
  installProvider,
399
+ providerSourceFingerprint,
388
400
  onProgress: (input, message) => input.onProgress?.(message),
389
401
  ...overrides
390
402
  };
@@ -431,15 +443,53 @@ async function installNeo4j({ paths, home, url, sha256 }) {
431
443
  await runCommand('tar', ['-xzf', archive, '-C', paths.nativeRuntimeDir]);
432
444
  }
433
445
 
434
- async function installProvider({ uvCommand, venvPath, providerPython, packageRoot }) {
446
+ async function installProvider({ uvCommand, venvPath, providerPython, providerSource }) {
435
447
  await mkdir(dirname(venvPath), { recursive: true });
436
448
  if (!existsSync(providerPython)) {
437
449
  await runCommand(uvCommand, ['venv', '--python', '3.12', '--seed', venvPath]);
438
450
  }
439
- await runCommand(uvCommand, [
451
+ await runCommand(uvCommand, nativeProviderInstallArguments({
452
+ providerPython,
453
+ providerSource
454
+ }));
455
+ }
456
+
457
+ export function nativeProviderInstallArguments({ providerPython, providerSource }) {
458
+ return [
440
459
  'pip', 'install', '--python', providerPython, '--upgrade',
441
- join(packageRoot, 'graph-provider')
442
- ]);
460
+ '--reinstall-package', 'fuli-graph-provider',
461
+ providerSource
462
+ ];
463
+ }
464
+
465
+ async function providerSourceFingerprint(providerSource) {
466
+ const files = [
467
+ join(providerSource, 'pyproject.toml'),
468
+ ...await pythonSourceFiles(join(providerSource, 'fuli_graph'))
469
+ ].sort();
470
+ const hash = createHash('sha256');
471
+ for (const path of files) {
472
+ const name = relative(providerSource, path).replaceAll('\\', '/');
473
+ hash.update(name);
474
+ hash.update('\0');
475
+ hash.update(await readFile(path));
476
+ hash.update('\0');
477
+ }
478
+ return hash.digest('hex');
479
+ }
480
+
481
+ async function pythonSourceFiles(directory) {
482
+ const entries = await readdir(directory, { withFileTypes: true });
483
+ const files = [];
484
+ for (const entry of entries) {
485
+ const path = join(directory, entry.name);
486
+ if (entry.isDirectory()) {
487
+ if (entry.name !== '__pycache__') files.push(...await pythonSourceFiles(path));
488
+ } else if (entry.isFile() && entry.name.endsWith('.py')) {
489
+ files.push(path);
490
+ }
491
+ }
492
+ return files;
443
493
  }
444
494
 
445
495
  async function downloadFile(url, destination) {