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
@@ -0,0 +1,107 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { EmployeeError, parseEmployeeManifest } from './manifest.js';
5
+
6
+ const builtinRoot = fileURLToPath(new URL('./catalog/', import.meta.url));
7
+
8
+ export function createEmployeePackageRegistry({ packageDirectory, dataDirectory, catalogDirectory = builtinRoot }) {
9
+ const runtimes = new Map();
10
+ let closed = false;
11
+
12
+ function catalog() {
13
+ const entries = new Map();
14
+ for (const file of readdirSync(catalogDirectory).filter((name) => name.endsWith('.json'))) {
15
+ const manifest = readManifest(join(catalogDirectory, file));
16
+ entries.set(manifest.id, { manifest, root: null, runtimeStatus: manifest.runtime ? 'install_required' : 'not_required' });
17
+ }
18
+ if (packageDirectory && existsSync(packageDirectory)) {
19
+ for (const name of readdirSync(packageDirectory)) {
20
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) continue;
21
+ const root = join(packageDirectory, name);
22
+ try {
23
+ if (!statSync(root).isDirectory()) continue;
24
+ confinedPath(packageDirectory, name);
25
+ const manifest = readManifest(confinedPath(root, 'employee.json'));
26
+ if (manifest.id !== name) continue;
27
+ if (manifest.runtime) {
28
+ confinedPath(root, manifest.runtime.entry);
29
+ confinedPath(root, `${manifest.runtime.webRoot}/index.html`);
30
+ }
31
+ entries.set(name, { manifest, root, runtimeStatus: manifest.runtime ? 'ready' : 'not_required' });
32
+ } catch {
33
+ const previous = entries.get(name);
34
+ if (previous) entries.set(name, { ...previous, runtimeStatus: 'unavailable' });
35
+ }
36
+ }
37
+ }
38
+ return [...entries.values()];
39
+ }
40
+
41
+ function get(id) {
42
+ const entry = catalog().find(({ manifest }) => manifest.id === id);
43
+ if (!entry) throw new EmployeeError('Employee template not found', 404, 'template_not_found');
44
+ return entry;
45
+ }
46
+
47
+ async function runtime(id) {
48
+ if (closed) throw new EmployeeError('Employee runtime is closed', 503, 'runtime_closed');
49
+ const entry = get(id);
50
+ if (entry.runtimeStatus !== 'ready' || !entry.manifest.runtime) {
51
+ throw new EmployeeError('Employee workbench package is not installed or unavailable', 503, 'runtime_unavailable');
52
+ }
53
+ if (!runtimes.has(id)) {
54
+ const loading = (async () => {
55
+ const module = await import(pathToFileURL(confinedPath(entry.root, entry.manifest.runtime.entry)).href);
56
+ if (typeof module.createEmployeeRuntime !== 'function') throw new TypeError('Invalid employee runtime export');
57
+ const instance = await module.createEmployeeRuntime({
58
+ dataDirectory: resolve(dataDirectory, id),
59
+ packageDirectory: entry.root,
60
+ manifest: entry.manifest
61
+ });
62
+ if (!instance || !['handleHttp', 'callTool', 'describeTools', 'close'].every(
63
+ (name) => typeof instance[name] === 'function'
64
+ )) {
65
+ instance?.close?.();
66
+ throw new TypeError('Employee runtime does not implement API version 1');
67
+ }
68
+ if (closed) { await instance.close(); throw new Error('Employee runtime is closed'); }
69
+ return instance;
70
+ })().catch(() => {
71
+ runtimes.delete(id);
72
+ throw new EmployeeError('Employee runtime could not be loaded', 503, 'runtime_unavailable');
73
+ });
74
+ runtimes.set(id, loading);
75
+ }
76
+ return runtimes.get(id);
77
+ }
78
+
79
+ return {
80
+ catalog, get, runtime,
81
+ async close() {
82
+ closed = true;
83
+ await Promise.allSettled([...runtimes.values()].map(async (entry) => (await entry).close()));
84
+ runtimes.clear();
85
+ }
86
+ };
87
+ }
88
+
89
+ export function confinedPath(root, file) {
90
+ const resolvedRoot = realpathSync(root);
91
+ const resolvedFile = realpathSync(resolve(root, file));
92
+ const local = relative(resolvedRoot, resolvedFile);
93
+ if (local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local)) {
94
+ throw new TypeError('Employee package path escapes its installation');
95
+ }
96
+ return resolvedFile;
97
+ }
98
+
99
+ function readManifest(file) {
100
+ if (statSync(file).size > 32_768) throw new TypeError('Employee manifest is too large');
101
+ return parseEmployeeManifest(JSON.parse(readFileSync(file, 'utf8')));
102
+ }
103
+
104
+ export function employeeDirectories(runtimeConfigPath) {
105
+ const root = join(dirname(runtimeConfigPath), 'employees');
106
+ return { packageDirectory: join(root, 'packages'), dataDirectory: join(root, 'data') };
107
+ }
@@ -0,0 +1,50 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { EmployeeError } from './manifest.js';
4
+
5
+ const projectId = z.string().min(1).max(256);
6
+ const selectionSchema = z.object({
7
+ personalProjectId: projectId.optional(),
8
+ personalProjectIds: z.array(projectId).max(500).optional(),
9
+ replaceAssignments: z.boolean().optional(),
10
+ expectedAssignmentsVersion: z.string().min(1).max(128).optional(),
11
+ reactivate: z.boolean().optional(),
12
+ management: z.unknown().optional(),
13
+ });
14
+
15
+ export function parseEmployeeProjectSelection(input) {
16
+ const parsed = selectionSchema.safeParse(input);
17
+ if (!parsed.success) throw new TypeError('Invalid employee project selection');
18
+ const value = parsed.data;
19
+ if ((value.personalProjectId && value.personalProjectIds !== undefined) ||
20
+ (value.management !== undefined && (value.personalProjectId || value.personalProjectIds !== undefined)) ||
21
+ (value.replaceAssignments && ((!value.personalProjectIds && value.management === undefined) || !value.expectedAssignmentsVersion)) ||
22
+ (!value.replaceAssignments && value.expectedAssignmentsVersion !== undefined)) {
23
+ throw new TypeError('Replacing employee projects requires an explicit project list and its last-read version');
24
+ }
25
+ return {
26
+ projectIds: [...new Set(value.personalProjectIds ?? (value.personalProjectId ? [value.personalProjectId] : []))],
27
+ replace: value.replaceAssignments === true,
28
+ expectedVersion: value.expectedAssignmentsVersion,
29
+ };
30
+ }
31
+
32
+ export function activeEmployeeAssignments(agent) {
33
+ return (agent?.assignments ?? []).filter(({ status }) => status === 'active');
34
+ }
35
+
36
+ export function employeeAssignmentsVersion(agent, policy = null) {
37
+ const records = activeEmployeeAssignments(agent)
38
+ .map(({ personalProjectId, assignmentId, revision }) => [personalProjectId, assignmentId, revision ?? 0])
39
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
40
+ return createHash('sha256').update(JSON.stringify(policy ? [records, policy] : records)).digest('hex');
41
+ }
42
+
43
+ export async function resolveEmployeeProjectSelection(app, spaceId, selection) {
44
+ const projects = await app.listPersonalProjects({ personalSpaceId: spaceId });
45
+ const availableIds = new Set(projects.filter((entry) => entry.profile?.lifecycle !== 'archived').map((entry) => entry.project_id));
46
+ if (selection.projectIds.some((id) => !availableIds.has(id))) {
47
+ throw new EmployeeError('Project is unavailable; reload and select active FULI projects', 404, 'project_not_found');
48
+ }
49
+ return availableIds;
50
+ }
@@ -0,0 +1,199 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { EmployeeError, employeeAgentId, employeeCapability } from './manifest.js';
3
+ import {
4
+ activeEmployeeAssignments, employeeAssignmentsVersion,
5
+ parseEmployeeProjectSelection, resolveEmployeeProjectSelection,
6
+ } from './project-selection.js';
7
+ import { managementFor, managementWithoutRevision, parseEmployeeManagement, policyAllowsProject } from './management-policy.js';
8
+
9
+ export function createEmployeeRecruitment({ app, registry, managementStore }) {
10
+ const locks = new Map();
11
+ const spaceId = app.config.personal.spaceId;
12
+
13
+ function assertSpace(input) {
14
+ if (input.personalSpaceId && input.personalSpaceId !== spaceId) {
15
+ throw new EmployeeError('Employee belongs to another personal space', 403, 'space_mismatch');
16
+ }
17
+ }
18
+
19
+ async function project(input, required = false) {
20
+ assertSpace(input);
21
+ const id = input.personalProjectId;
22
+ if (!id && !required) return null;
23
+ if (!id) throw new EmployeeError('Select a project before opening the workbench', 400, 'project_required');
24
+ const projects = await app.listPersonalProjects({ personalSpaceId: spaceId });
25
+ const match = projects.find((entry) => entry.project_id === id);
26
+ if (!match || match.profile?.lifecycle === 'archived') {
27
+ throw new EmployeeError('Project is unavailable; create or select an active FULI project', 404, 'project_not_found');
28
+ }
29
+ return { id: match.project_id, name: match.profile.name, description: match.profile.purpose ?? '' };
30
+ }
31
+
32
+ async function list(input = {}) {
33
+ assertSpace(input);
34
+ const [agents, projects] = await Promise.all([
35
+ app.listProjectAgents({ personalSpaceId: spaceId }),
36
+ app.listPersonalProjects({ personalSpaceId: spaceId }),
37
+ ]);
38
+ return { templates: registry.catalog().map(({ manifest, runtimeStatus }) => {
39
+ const agent = agents.find(({ agentId }) => agentId === employeeAgentId(manifest.id));
40
+ const owned = agent?.profile.capabilities.includes(employeeCapability(manifest.id));
41
+ const stored = managementStore.read(spaceId, manifest.id);
42
+ const management = managementFor(manifest, owned ? agent : null, stored);
43
+ const managedProjects = owned && agent.profile.status === 'active' ? projects
44
+ .filter(entry => entry.profile?.lifecycle !== 'archived' && policyAllowsProject(management, entry.project_id)
45
+ && (management.mode === 'all' || activeEmployeeAssignments(agent).some(a => a.personalProjectId === entry.project_id)))
46
+ .map(entry => ({ id: entry.project_id, name: entry.profile.name })) : [];
47
+ return {
48
+ ...manifest,
49
+ runtime: manifest.runtime ? { apiVersion: manifest.runtime.apiVersion } : null,
50
+ runtimeStatus,
51
+ agentId: owned ? agent.agentId : null,
52
+ agentStatus: owned ? agent.profile.status : null,
53
+ assignmentsVersion: employeeAssignmentsVersion(owned ? agent : null, stored),
54
+ management, managedProjects,
55
+ assignmentCount: owned ? activeEmployeeAssignments(agent).length : 0,
56
+ assignments: owned ? activeEmployeeAssignments(agent) : [],
57
+ identityConflict: Boolean(agent && !owned)
58
+ };
59
+ }) };
60
+ }
61
+
62
+ async function recruit(input) {
63
+ assertSpace(input);
64
+ const { manifest } = registry.get(input.templateId);
65
+ const selection = parseEmployeeProjectSelection(input);
66
+ const agentId = employeeAgentId(manifest.id);
67
+ const key = `${spaceId}:${agentId}`;
68
+ const previous = locks.get(key) ?? Promise.resolve();
69
+ const operation = previous.catch(() => {}).then(async () => {
70
+ // Validate the entire snapshot before creating an identity or changing any assignment.
71
+ const availableIds = await resolveEmployeeProjectSelection(app, spaceId, selection);
72
+ const agents = await app.listProjectAgents({ personalSpaceId: spaceId });
73
+ let agent = agents.find((entry) => entry.agentId === agentId);
74
+ let changed = false;
75
+ if (agent && !agent.profile.capabilities.includes(employeeCapability(manifest.id))) {
76
+ throw new EmployeeError('This Agent identifier is already in use; the existing Agent was not modified', 409, 'identity_conflict');
77
+ }
78
+ if (agent && agent.profile.status !== 'active' && input.reactivate !== true) {
79
+ throw new EmployeeError('Confirm reactivation of this employee first', 409, 'reactivation_required');
80
+ }
81
+ const active = activeEmployeeAssignments(agent);
82
+ const stored = managementStore.read(spaceId, manifest.id);
83
+ const currentPolicy = managementFor(manifest, agent, stored);
84
+ let desiredPolicy = managementWithoutRevision(currentPolicy);
85
+ if (input.management !== undefined) {
86
+ desiredPolicy = parseEmployeeManagement(input.management);
87
+ if (agent && !selection.replace) throw new TypeError('Updating employee management requires replaceAssignments and its last-read version');
88
+ const existingProjects = new Set((await app.listPersonalProjects({ personalSpaceId: spaceId })).map(p => p.project_id));
89
+ if (desiredPolicy.projectIds.some(id => !availableIds.has(id)) || desiredPolicy.excludedProjectIds.some(id => !existingProjects.has(id) && !currentPolicy.excludedProjectIds.includes(id))) {
90
+ throw new EmployeeError('Project is unavailable; reload the management policy', 404, 'project_not_found');
91
+ }
92
+ } else if (input.personalProjectIds !== undefined || input.personalProjectId) {
93
+ if (currentPolicy.mode === 'all' && agent && !selection.replace) {
94
+ if (selection.projectIds.some(id => !policyAllowsProject(currentPolicy, id))) {
95
+ throw new EmployeeError('This project is excluded; update the management policy explicitly', 403, 'project_excluded');
96
+ }
97
+ } else {
98
+ desiredPolicy = { ...desiredPolicy, mode: 'selected', excludedProjectIds: [],
99
+ projectIds: [...new Set(selection.replace ? selection.projectIds : [...active.map(a => a.personalProjectId), ...selection.projectIds])].sort() };
100
+ }
101
+ }
102
+ const requestedIds = new Set([...availableIds].filter(id => policyAllowsProject(desiredPolicy, id)));
103
+ const additions = [...requestedIds].filter((id) => !active.some((entry) => entry.personalProjectId === id));
104
+ // Archived project history stays intact; this control only manages currently available projects.
105
+ const removals = selection.replace ? active.filter((entry) => availableIds.has(entry.personalProjectId) && !requestedIds.has(entry.personalProjectId)) : [];
106
+ const policyChanged = JSON.stringify(desiredPolicy) !== JSON.stringify(managementWithoutRevision(currentPolicy));
107
+ if (selection.replace && (additions.length || removals.length || policyChanged) &&
108
+ selection.expectedVersion !== employeeAssignmentsVersion(agent, stored)) {
109
+ throw new EmployeeError('Project assignments changed; reload the selection before saving', 409, 'assignment_scope_conflict');
110
+ }
111
+ if (!agent || agent.profile.status !== 'active') {
112
+ const profile = agent ? { ...agent.profile, status: 'active' } : {
113
+ name: manifest.name,
114
+ occupationEmoji: manifest.occupationEmoji,
115
+ responsibility: `${manifest.role}:${manifest.description}`,
116
+ agentType: 'durable', status: 'active',
117
+ capabilities: [...manifest.capabilities, employeeCapability(manifest.id)],
118
+ workKinds: manifest.workKinds,
119
+ initialPreferences: manifest.initialPreferences,
120
+ defaultModelStrategy: { mode: 'adaptive' },
121
+ executorPolicy: { mode: 'flexible' }
122
+ };
123
+ agent = await app.upsertProjectAgent({ personalSpaceId: spaceId, agentId, profile });
124
+ changed = true;
125
+ }
126
+ const endedAssignments = [];
127
+ try {
128
+ // Persist the authorization boundary first. Provider writes may fail,
129
+ // but a newly excluded project must immediately stop reaching the PM.
130
+ if (policyChanged || !stored) {
131
+ managementStore.write(spaceId, manifest.id, desiredPolicy, stored?.revision ?? 0);
132
+ changed = true;
133
+ }
134
+ // Revoke first: an interrupted batch must never grant new access before removing exclusions.
135
+ for (const assignment of removals) {
136
+ endedAssignments.push(await app.endProjectAgentAssignment({
137
+ personalSpaceId: spaceId, personalProjectId: assignment.personalProjectId,
138
+ assignmentId: assignment.assignmentId, expectedRevision: assignment.revision ?? 0,
139
+ reason: `Update employee ${manifest.id} project selection`,
140
+ }));
141
+ changed = true;
142
+ }
143
+ for (const personalProjectId of additions) {
144
+ const history = (agent.assignments ?? []).filter((entry) => entry.personalProjectId === personalProjectId);
145
+ const generation = history.map((entry) => `${entry.assignmentId}:${entry.revision ?? 0}`).sort();
146
+ const key = createHash('sha256').update(JSON.stringify([manifest.id, personalProjectId, generation])).digest('hex');
147
+ await app.createProjectAgentAssignment({
148
+ personalSpaceId: spaceId, personalProjectId, agentId,
149
+ idempotencyKey: `employee-assignment:${key}`,
150
+ responsibility: agent.profile.responsibility,
151
+ workKinds: agent.profile.workKinds ?? manifest.workKinds,
152
+ capabilities: agent.profile.capabilities,
153
+ reason: `Recruit employee template ${manifest.id}@${manifest.version}`,
154
+ });
155
+ changed = true;
156
+ }
157
+ } catch (error) {
158
+ if (error instanceof EmployeeError && error.code === 'assignment_scope_conflict') throw error;
159
+ // Provider assignment writes are individually durable, not a cross-project transaction.
160
+ throw new EmployeeError('Project selection could not be fully saved; reload actual assignments before retrying', 502, 'assignment_update_incomplete');
161
+ }
162
+ agent = (await app.listProjectAgents({ personalSpaceId: spaceId })).find((entry) => entry.agentId === agentId);
163
+ const assignments = activeEmployeeAssignments(agent);
164
+ const assignment = selection.projectIds.length === 1
165
+ ? assignments.find((entry) => entry.personalProjectId === selection.projectIds[0]) ?? null : null;
166
+ return {
167
+ templateId: manifest.id, management: managementFor(manifest, agent, managementStore.read(spaceId, manifest.id)),
168
+ assignmentsVersion: employeeAssignmentsVersion(agent, managementStore.read(spaceId, manifest.id)),
169
+ assignmentCount: assignments.length, endedAssignmentCount: endedAssignments.length, idempotent: !changed,
170
+ assignments: structuredClone(assignments), endedAssignments: structuredClone(endedAssignments),
171
+ assignment: structuredClone(assignment), agent,
172
+ };
173
+ });
174
+ locks.set(key, operation);
175
+ try { return await operation; }
176
+ finally { if (locks.get(key) === operation) locks.delete(key); }
177
+ }
178
+
179
+ async function authorize(input) {
180
+ const selectedProject = await project(input, true);
181
+ const { manifest } = registry.get(input.templateId);
182
+ // All-project policy also covers projects without a materialized assignment.
183
+ const agents = await app.listProjectAgents({ personalSpaceId: spaceId });
184
+ const agent = agents.find((entry) => entry.agentId === employeeAgentId(manifest.id));
185
+ const management = managementFor(manifest, agent, managementStore.read(spaceId, manifest.id));
186
+ if (input.sourceApplication && Array.isArray(agent?.profile.allowedClients) && !agent.profile.allowedClients.includes(input.sourceApplication)) {
187
+ throw new EmployeeError('This employee is not enabled for the current Agent client', 403, 'assignment_required');
188
+ }
189
+ if (!agent || agent.profile.status !== 'active' ||
190
+ !agent.profile.capabilities.includes(employeeCapability(manifest.id)) ||
191
+ !policyAllowsProject(management, selectedProject.id) ||
192
+ (management.mode !== 'all' && !(agent.assignments ?? []).some((entry) => entry.personalProjectId === selectedProject.id && entry.status === 'active'))) {
193
+ throw new EmployeeError('Recruit this employee into the selected project first', 403, 'assignment_required');
194
+ }
195
+ return { personalSpaceId: spaceId, project: selectedProject, agentId: agent.agentId, management };
196
+ }
197
+
198
+ return { list, recruit, authorize };
199
+ }
@@ -0,0 +1,83 @@
1
+ import { EmployeeError } from './manifest.js';
2
+ import { createEmployeePackageRegistry, employeeDirectories } from './package-registry.js';
3
+ import { createEmployeeRecruitment } from './recruitment.js';
4
+ import { join } from 'node:path';
5
+ import { EmployeeManagementStore } from './management-policy.js';
6
+ import { employeeTaskEntry } from './task-entry.js';
7
+
8
+ export function createEmployeeService({ app, runtimeConfigPath, registry: configuredRegistry, managementStore: configuredManagementStore }) {
9
+ const registry = configuredRegistry ?? createEmployeePackageRegistry(employeeDirectories(runtimeConfigPath));
10
+ const managementStore = configuredManagementStore ?? new EmployeeManagementStore(runtimeConfigPath
11
+ ? join(employeeDirectories(runtimeConfigPath).dataDirectory, 'management.sqlite') : ':memory:');
12
+ const recruitment = createEmployeeRecruitment({ app, registry, managementStore });
13
+
14
+ async function resolveInput(input) {
15
+ if (!input.projectPath) return input;
16
+ const resolution = await app.listCurrentProjectAgents({ projectPath: input.projectPath });
17
+ if (!resolution.personal_project_id) {
18
+ throw new EmployeeError('Register or select this exact FULI project first', 400, 'project_unresolved');
19
+ }
20
+ if (input.personalProjectId && input.personalProjectId !== resolution.personal_project_id) {
21
+ throw new EmployeeError('Project path and project identifier do not match', 403, 'project_mismatch');
22
+ }
23
+ return { ...input, personalProjectId: resolution.personal_project_id };
24
+ }
25
+
26
+ async function workspace(input) {
27
+ const resolved = await resolveInput(input);
28
+ const context = await recruitment.authorize(resolved);
29
+ const { manifest, runtimeStatus } = registry.get(resolved.templateId);
30
+ const basePath = `/employee-workspaces/${encodeURIComponent(manifest.id)}/${encodeURIComponent(context.project.id)}/`;
31
+ return {
32
+ ...context, templateId: manifest.id, name: manifest.name, role: manifest.role,
33
+ runtimeStatus, basePath,
34
+ workbenchUrl: basePath,
35
+ agentCardUrl: `${basePath}.well-known/agent-card.json`
36
+ };
37
+ }
38
+
39
+ async function describeTools(input) {
40
+ const context = await workspace(input);
41
+ const runtime = await registry.runtime(context.templateId);
42
+ return { ...context, tools: await runtime.describeTools() };
43
+ }
44
+
45
+ async function callTool(input) {
46
+ const context = await workspace(input);
47
+ const runtime = await registry.runtime(context.templateId);
48
+ const tools = await runtime.describeTools();
49
+ const tool = tools.find((entry) => entry.name === input.tool);
50
+ const { manifest } = registry.get(context.templateId);
51
+ if (!tool || !manifest.permissions.includes(tool.permission)) {
52
+ throw new EmployeeError('Employee tool is not available or permitted', 403, 'tool_not_permitted');
53
+ }
54
+ const session = input.sourceSessionId ? {
55
+ sourceApplication: input.sourceApplication ?? 'other', id: input.sourceSessionId,
56
+ verified: input.sourceSessionVerified === true,
57
+ } : null;
58
+ try { return await runtime.callTool(input.tool, input.arguments ?? {}, { ...context, session }); }
59
+ catch (error) {
60
+ if (Number.isInteger(error?.status) && error.status >= 400 && error.status < 500) {
61
+ throw new EmployeeError(error.message, error.status, error.code ?? 'tool_error');
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ return {
68
+ list: recruitment.list,
69
+ recruit: async (input) => recruitment.recruit(await resolveInput(input)),
70
+ workspace, describeTools, callTool,
71
+ authorize: recruitment.authorize,
72
+ taskEntry: (input) => employeeTaskEntry({ registry, recruitment, workspace }, input)
73
+ .catch(() => ({ status: 'unavailable', managers: [], worker_started: false })),
74
+ async handleHttp(request, response, input) {
75
+ const context = await workspace(input);
76
+ const runtime = await registry.runtime(context.templateId);
77
+ await runtime.handleHttp(request, response, {
78
+ ...context, origin: input.origin, relativePath: input.relativePath
79
+ });
80
+ },
81
+ async close() { try { await registry.close(); } finally { managementStore.close(); } }
82
+ };
83
+ }
@@ -0,0 +1,39 @@
1
+ // Management is a task-entry capability, not a replacement for the specialist
2
+ // role/executor chosen by FULI. Never persist the user's prompt or start a model.
3
+ export async function employeeTaskEntry({ registry, recruitment, workspace }, input) {
4
+ if (!input.personalProjectId) return { status: 'project_unresolved', managers: [], worker_started: false };
5
+ const managers = [];
6
+ let unavailable = false;
7
+ for (const { manifest, runtimeStatus } of registry.catalog()) {
8
+ if (!manifest.taskEntry) continue;
9
+ try {
10
+ const context = await recruitment.authorize({ templateId: manifest.id, personalProjectId: input.personalProjectId, sourceApplication: input.sourceApplication });
11
+ if (runtimeStatus !== 'ready') { unavailable = true; continue; }
12
+ const resolved = await workspace({ templateId: manifest.id, personalProjectId: input.personalProjectId, sourceApplication: input.sourceApplication });
13
+ const runtime = await registry.runtime(manifest.id);
14
+ const definition = (await runtime.describeTools()).find(tool => tool.name === manifest.taskEntry.boardTool);
15
+ if (!definition || definition.permission !== 'board.read' || !manifest.permissions.includes('board.read')) {
16
+ unavailable = true; continue;
17
+ }
18
+ const board = await runtime.callTool(manifest.taskEntry.boardTool, { limit: 8 }, resolved);
19
+ managers.push({
20
+ template_id: manifest.id, agent_id: context.agentId, name: manifest.name,
21
+ personal_project_id: context.project.id, scope_mode: context.management.mode,
22
+ title_mode: context.management.titleMode, title_style: context.management.titleStyle,
23
+ board: { version: board.version, total: board.total, truncated: board.truncated,
24
+ items: (board.items ?? []).slice(0, 8).map(item => ({
25
+ id: item.id, title: item.title, status: item.status, priority: item.priority,
26
+ workType: item.workType, updatedAt: item.updatedAt,
27
+ })) },
28
+ session: { sourceApplication: input.sourceApplication ?? 'other', sessionId: input.sourceSessionId ?? input.sessionId ?? null },
29
+ tools: { discover: 'list_employee_tools', call: 'call_employee_tool',
30
+ read_board: manifest.taskEntry.boardTool, prepare_title: manifest.taskEntry.titleTool ?? null },
31
+ guidance: 'Route actionable work in this project through this manager: match existing board items, or create a concise task for new work. Keep conversation-only questions out of the backlog. Update linked items as evidence arrives; Agent completion goes to review, never human-confirmed done. Before creating or editing, discover the installed tool schemas and preserve versions/idempotency. If title_mode is auto or suggest, prepare a title from only this session’s linked work item IDs. Auto applies only when the current host exposes a native session rename capability and the exact target session is known: execute the returned action in that host, then report its actual result. Never edit client databases or transcripts, invent an execution receipt, rename another session, or override a locked/manual title. Do not start an additional model solely for this entry.'
32
+ });
33
+ } catch (error) {
34
+ // Excluded/unassigned/inactive projects never reach the plugin runtime.
35
+ if (!['assignment_required', 'project_not_found'].includes(error?.code)) unavailable = true;
36
+ }
37
+ }
38
+ return { status: unavailable ? 'degraded' : managers.length ? 'ready' : 'not_managed', managers, worker_started: false };
39
+ }