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.
- package/README.md +69 -13
- package/README.zh-CN.md +60 -14
- package/dist/web/assets/{AboutPage-sLV9WChi.js → AboutPage-BcZeFcHe.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-xAQYl1SQ.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-CbPAmhy7.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-BQvTLoc8.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-DF3EL0Ui.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-UnnBDt0I.js +1 -0
- package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
- package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-BqPJH5Rr.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
- package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
- package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-Xyfm5xkZ.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-DB9mByB3.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-_-YXVi5O.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DeeQByPX.js +11 -0
- package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
- package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-CQYGwFBH.js} +1 -1
- package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
- package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-BlEZ03Yi.js} +1 -1
- package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-DvRt_nmP.js} +1 -1
- package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
- package/dist/web/assets/{console-BT1P2Db4.js → console-tOxOl8Vq.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
- package/dist/web/assets/{i18n-js6gfc7j.js → i18n-CgBHZthI.js} +3 -3
- package/dist/web/assets/{identity-CfDfanpa.js → identity-CFo_Uw1H.js} +1 -1
- package/dist/web/assets/index-CJc5ppXH.css +1 -0
- package/dist/web/assets/index-yCi3cz2l.js +2 -0
- package/dist/web/assets/{model-DJkLAJsg.js → model-BtnVr7fX.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
- package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzHm-y-x.js} +1 -1
- package/dist/web/index.html +7 -6
- package/docs/employee-agents.md +99 -0
- package/docs/project-agent-memory.md +479 -0
- package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
- package/graph-provider/fuli_graph/model_base.py +5 -0
- package/graph-provider/fuli_graph/model_validation.py +32 -0
- package/graph-provider/fuli_graph/models.py +26 -170
- package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
- package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
- package/graph-provider/fuli_graph/project_agent_models.py +9 -0
- package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +21 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +217 -29
- package/graph-provider/fuli_graph/runtime.py +37 -1
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
- package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
- package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
- package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +748 -162
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
- package/graph-provider/fuli_graph/store_project_agents.py +214 -46
- package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
- package/graph-provider/fuli_graph/store_transactions.py +40 -0
- package/graph-provider/fuli_graph/task_context_models.py +50 -0
- package/graph-provider/fuli_graph/task_context_routes.py +31 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +8 -8
- package/package.json +5 -2
- package/skills/capturing-session-knowledge/SKILL.md +18 -6
- package/src/adaptive-runtime/executor-pool.js +46 -5
- package/src/adaptive-runtime/graph-services.js +10 -3
- package/src/adaptive-runtime/lease-client.js +66 -14
- package/src/adaptive-runtime/runtime-broker.js +24 -9
- package/src/agent-tools/employee-definitions.js +43 -0
- package/src/agent-tools/graph-definitions.js +25 -6
- package/src/agent-tools/graph-handlers.js +11 -0
- package/src/agent-tools/project-agent-definitions.js +35 -4
- package/src/agent-tools/project-agent-memory-definitions.js +36 -0
- package/src/agent-tools.js +6 -2
- package/src/agents/codex/lifecycle-hook.js +107 -0
- package/src/agents/codex/lifecycle-hooks.js +231 -0
- package/src/agents/cursor/lifecycle-hook.js +133 -0
- package/src/agents/cursor/lifecycle-hooks.js +50 -0
- package/src/agents/lifecycle-message.js +19 -0
- package/src/app/agent-request-context.js +23 -0
- package/src/cli/command-registry.js +2 -0
- package/src/cli/employee-command.js +17 -0
- package/src/cli/remote-mcp-command.js +282 -0
- package/src/cli/setup-command.js +5 -0
- package/src/cli.js +10 -0
- package/src/employees/catalog/jefa.json +21 -0
- package/src/employees/install-package.js +77 -0
- package/src/employees/management-policy.js +88 -0
- package/src/employees/manifest.js +53 -0
- package/src/employees/package-registry.js +107 -0
- package/src/employees/project-selection.js +50 -0
- package/src/employees/recruitment.js +199 -0
- package/src/employees/service.js +83 -0
- package/src/employees/task-entry.js +39 -0
- package/src/graphiti/agent-knowledge-workflows.js +135 -25
- package/src/graphiti/federated-application.js +78 -35
- package/src/graphiti/project-agent-control-plane.js +20 -3
- package/src/graphiti/project-agent-mapping.js +155 -5
- package/src/graphiti/project-agent-memory.js +115 -0
- package/src/graphiti/project-agent-task-entry.js +126 -0
- package/src/graphiti/project-agent-workflows.js +30 -14
- package/src/graphiti/provider-client.js +66 -4
- package/src/http/api-router.js +25 -5
- package/src/http/employee-api-router.js +64 -0
- package/src/http/error-mapping.js +4 -0
- package/src/http/graph-api-router.js +2 -0
- package/src/http/request-policy.js +6 -3
- package/src/http/static-handler.js +4 -3
- package/src/mcp/create-mcp-server.js +98 -37
- package/src/mcp/direct-call.js +38 -0
- package/src/mcp/instructions.js +8 -7
- package/src/mcp/provider-task-context-registry.js +74 -0
- package/src/mcp/remote-http-server.js +761 -0
- package/src/mcp/session-id.js +29 -3
- package/src/mcp/task-context-registry.js +88 -29
- package/src/mcp/tool-annotations.js +10 -2
- package/src/mcp/tool-result.js +8 -3
- package/src/mcp-server.js +20 -8
- package/src/native-runtime/runtime.js +58 -8
- package/src/server.js +76 -28
- package/src/setup/agent-installation-status.js +38 -6
- package/src/setup/agents.js +20 -8
- package/src/setup/claude-code-config.js +55 -13
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +74 -7
- package/src/setup/config-backup.js +18 -2
- package/src/setup/cursor-config.js +22 -5
- package/src/setup/setup.js +30 -22
- package/src/storage/json-file.js +11 -3
- package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
- package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
- package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
- package/dist/web/assets/index-BnH5mcZx.css +0 -1
- package/dist/web/assets/index-GqJf1xqg.js +0 -2
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
|
|
4
|
+
import { agentMemoryRecord, prepareProjectAgentMemory } from './project-agent-memory.js';
|
|
5
|
+
|
|
3
6
|
import { onlineSourceUri } from './source-uri.js';
|
|
4
7
|
import {
|
|
5
8
|
relatedProjectGuidance,
|
|
@@ -8,28 +11,58 @@ import {
|
|
|
8
11
|
|
|
9
12
|
export async function beginTaskContext(application, {
|
|
10
13
|
sessionId,
|
|
14
|
+
turnId = null,
|
|
11
15
|
projectPath,
|
|
12
|
-
|
|
16
|
+
personalProjectId = null,
|
|
17
|
+
taskPrompt = null,
|
|
18
|
+
sourceApplication = 'other',
|
|
19
|
+
sourceSessionId = null,
|
|
20
|
+
projectAgentId = null,
|
|
21
|
+
workKind = null,
|
|
22
|
+
requiredCapabilities = []
|
|
13
23
|
}) {
|
|
24
|
+
const continuationToken = /^FULI_CHECKPOINT_REQUIRED: (fuli-task-[a-zA-Z0-9-]{8,128})\b/.exec(taskPrompt ?? '')?.[1];
|
|
25
|
+
const resumed = continuationToken
|
|
26
|
+
? await application.taskContextRegistry.context(continuationToken, sourceApplication) : null;
|
|
27
|
+
if (resumed && resumed.sessionId !== sessionId) {
|
|
28
|
+
throw validationError('Checkpoint continuation belongs to another host session');
|
|
29
|
+
}
|
|
14
30
|
const preferences = await application.getCollaborationPreferences({
|
|
31
|
+
personalProjectId,
|
|
15
32
|
projectPath,
|
|
33
|
+
sessionId,
|
|
34
|
+
turnId,
|
|
16
35
|
taskPrompt,
|
|
36
|
+
projectAgentId: resumed?.projectAgentId ?? projectAgentId,
|
|
37
|
+
sourceApplication,
|
|
38
|
+
sourceSessionId,
|
|
39
|
+
workKind,
|
|
40
|
+
requiredCapabilities,
|
|
17
41
|
agentInvocation: true,
|
|
18
42
|
agentToolName: 'begin_task_context'
|
|
19
43
|
});
|
|
20
|
-
|
|
44
|
+
if (resumed && resumed.personalProjectId !== preferences.context.personal_project_id) {
|
|
45
|
+
throw validationError('Checkpoint continuation belongs to another project');
|
|
46
|
+
}
|
|
47
|
+
const task = resumed ?? await application.taskContextRegistry.begin({
|
|
21
48
|
sessionId,
|
|
22
|
-
|
|
49
|
+
turnId,
|
|
50
|
+
personalProjectId: preferences.context.personal_project_id,
|
|
51
|
+
projectAgentId: preferences.context.project_agent_id,
|
|
52
|
+
sourceApplication,
|
|
53
|
+
sourceSessionId,
|
|
54
|
+
memoryRevision: preferences.project_agent_context?.memory?.revision ?? null
|
|
23
55
|
});
|
|
24
56
|
return {
|
|
25
57
|
taskContextToken: task.token,
|
|
26
58
|
task_context_token: task.token,
|
|
27
|
-
checkpoint_required:
|
|
59
|
+
checkpoint_required: task.checkpoint?.phase !== 'complete',
|
|
60
|
+
resumed_checkpoint: Boolean(resumed),
|
|
28
61
|
previous_checkpoint_missing: task.previousCheckpointMissing,
|
|
29
62
|
...preferences,
|
|
30
63
|
task_guidance: {
|
|
31
64
|
retrieval: 'Inspect task_knowledge_recall before asking for a stable project fact or method again. On a miss, use search_current_project_knowledge with focused action, artifact, target-system, or identifier queries; never use the full conversational request as the only query.',
|
|
32
|
-
checkpoint: 'Before finishing, call checkpoint_task_knowledge with capture_candidates or retain_nothing.'
|
|
65
|
+
checkpoint: 'Before finishing, call checkpoint_task_knowledge with capture_candidates or retain_nothing. When durable role context changed, include agentMemory with the loaded revision and a bounded merged summary, decisions, open threads and next actions. Do not overwrite from truncated context or store raw transcripts.'
|
|
33
66
|
}
|
|
34
67
|
};
|
|
35
68
|
}
|
|
@@ -38,43 +71,120 @@ export async function checkpointTaskKnowledge(application, {
|
|
|
38
71
|
taskContextToken,
|
|
39
72
|
disposition,
|
|
40
73
|
reason,
|
|
41
|
-
capture = null
|
|
74
|
+
capture = null,
|
|
75
|
+
agentMemory = null,
|
|
76
|
+
sourceApplication = 'other',
|
|
77
|
+
personalProjectId = null,
|
|
78
|
+
remoteSessionId = null
|
|
42
79
|
}) {
|
|
43
80
|
if (!['capture_candidates', 'retain_nothing'].includes(disposition)) {
|
|
44
|
-
throw
|
|
81
|
+
throw validationError('Unknown task knowledge checkpoint disposition');
|
|
82
|
+
}
|
|
83
|
+
const task = await application.taskContextRegistry.context(taskContextToken, sourceApplication);
|
|
84
|
+
if (personalProjectId && task.personalProjectId !== personalProjectId) {
|
|
85
|
+
throw validationError('Task checkpoint belongs to another project');
|
|
86
|
+
}
|
|
87
|
+
if (remoteSessionId && task.sessionId !== remoteSessionId) {
|
|
88
|
+
throw validationError('Task checkpoint belongs to another remote session');
|
|
45
89
|
}
|
|
46
|
-
const task = application.taskContextRegistry.context(taskContextToken);
|
|
47
|
-
|
|
48
|
-
let captureResult = null;
|
|
49
90
|
if (disposition === 'capture_candidates') {
|
|
50
91
|
if (!capture) {
|
|
51
|
-
throw
|
|
92
|
+
throw validationError('capture_candidates requires a bounded capture payload');
|
|
93
|
+
}
|
|
94
|
+
} else if (capture) {
|
|
95
|
+
throw validationError('retain_nothing cannot include a capture payload');
|
|
96
|
+
}
|
|
97
|
+
const fingerprint = createHash('sha256').update(JSON.stringify(
|
|
98
|
+
canonicalCheckpoint({ disposition, reason, capture, agentMemory })
|
|
99
|
+
)).digest('hex');
|
|
100
|
+
if (task.checkpoint && task.checkpoint.fingerprint !== fingerprint) {
|
|
101
|
+
throw validationError(
|
|
102
|
+
'Task checkpoint already has different input; resume the original review or begin a new task context'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (task.checkpoint?.phase === 'complete') {
|
|
106
|
+
return { status: 'checkpointed', disposition, reason,
|
|
107
|
+
personal_project_id: task.personalProjectId, project_agent_id: task.projectAgentId,
|
|
108
|
+
replayed: true, capture_status: task.checkpoint.captureStatus };
|
|
109
|
+
}
|
|
110
|
+
const captureInput = capture ? {
|
|
111
|
+
...capture,
|
|
112
|
+
targetKind: 'personal',
|
|
113
|
+
spaceId: application.config.personal.spaceId,
|
|
114
|
+
personalProjectId: task.personalProjectId,
|
|
115
|
+
projectAgentId: task.projectAgentId ?? null,
|
|
116
|
+
sessionId: task.sessionId,
|
|
117
|
+
sourceApplication,
|
|
118
|
+
idempotencyKey: `${task.token}:knowledge`
|
|
119
|
+
} : null;
|
|
120
|
+
// Local validation has no side effects. Reject correctable input before
|
|
121
|
+
// binding an immutable checkpoint or advancing the Agent's memory revision.
|
|
122
|
+
if (captureInput) application.validateCaptureSessionKnowledge(captureInput);
|
|
123
|
+
let memoryResult = null;
|
|
124
|
+
let memoryRequest = null;
|
|
125
|
+
if (agentMemory) {
|
|
126
|
+
if (!task.projectAgentId || !task.personalProjectId) {
|
|
127
|
+
throw validationError('Task has no durable project Agent; do not guess a memory target');
|
|
52
128
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
spaceId: application.config.personal.spaceId,
|
|
129
|
+
const prepared = await prepareProjectAgentMemory(application, {
|
|
130
|
+
personalSpaceId: application.config.personal.spaceId,
|
|
56
131
|
personalProjectId: task.personalProjectId,
|
|
57
|
-
|
|
58
|
-
|
|
132
|
+
agentId: task.projectAgentId,
|
|
133
|
+
expectedRevision: agentMemory.expectedRevision,
|
|
134
|
+
idempotencyKey: `${task.token}:memory`,
|
|
135
|
+
memory: agentMemory.memory, sourceApplication,
|
|
136
|
+
// The logical client session survives independent lifecycle-hook MCP
|
|
137
|
+
// processes; their host session ids do not. The Provider persists this
|
|
138
|
+
// stable task session as the memory checkpoint provenance.
|
|
139
|
+
sourceSessionId: task.sessionId
|
|
59
140
|
});
|
|
60
|
-
|
|
61
|
-
|
|
141
|
+
if (prepared.status === 'capture_disabled') memoryResult = prepared;
|
|
142
|
+
else memoryRequest = { expected_revision: prepared.request.expected_revision,
|
|
143
|
+
memory: prepared.request.memory };
|
|
62
144
|
}
|
|
63
|
-
|
|
64
|
-
application.taskContextRegistry.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
145
|
+
const checkpoint = { disposition, reason, fingerprint, captureStatus: null };
|
|
146
|
+
const preparedTask = await application.taskContextRegistry.prepare(
|
|
147
|
+
taskContextToken, checkpoint, sourceApplication, memoryRequest
|
|
148
|
+
);
|
|
149
|
+
if (memoryRequest) {
|
|
150
|
+
if (!preparedTask.agentMemory) {
|
|
151
|
+
throw new TypeError('Provider did not confirm atomic Agent memory preparation');
|
|
152
|
+
}
|
|
153
|
+
memoryResult = { status: 'checkpointed', ...agentMemoryRecord(preparedTask.agentMemory) };
|
|
154
|
+
}
|
|
155
|
+
const captureResult = captureInput
|
|
156
|
+
? await application.captureSessionKnowledge(captureInput) : null;
|
|
157
|
+
await application.taskContextRegistry.checkpoint(taskContextToken, {
|
|
158
|
+
...checkpoint, captureStatus: captureResult?.status ?? null
|
|
159
|
+
}, sourceApplication);
|
|
69
160
|
return {
|
|
70
161
|
status: 'checkpointed',
|
|
71
162
|
disposition,
|
|
72
163
|
reason,
|
|
73
164
|
personal_project_id: task.personalProjectId,
|
|
74
|
-
|
|
165
|
+
project_agent_id: task.projectAgentId ?? null,
|
|
166
|
+
capture: captureResult,
|
|
167
|
+
agent_memory: memoryResult ? {
|
|
168
|
+
status: memoryResult.status, agentId: memoryResult.agentId,
|
|
169
|
+
checkpointId: memoryResult.checkpointId, revision: memoryResult.revision,
|
|
170
|
+
sourceApplication: memoryResult.sourceApplication,
|
|
171
|
+
sourceSessionId: memoryResult.sourceSessionId, createdAt: memoryResult.createdAt
|
|
172
|
+
} : null
|
|
75
173
|
};
|
|
76
174
|
}
|
|
77
175
|
|
|
176
|
+
function canonicalCheckpoint(value) {
|
|
177
|
+
if (Array.isArray(value)) return value.map(canonicalCheckpoint);
|
|
178
|
+
if (!value || typeof value !== 'object') return value;
|
|
179
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [
|
|
180
|
+
key, canonicalCheckpoint(value[key])
|
|
181
|
+
]));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validationError(message) {
|
|
185
|
+
return new ApplicationError(ApplicationErrorCode.VALIDATION, message);
|
|
186
|
+
}
|
|
187
|
+
|
|
78
188
|
export async function recordDecisionTrace(application, input) {
|
|
79
189
|
const trace = decisionTraceKnowledge(input);
|
|
80
190
|
return application.captureSessionKnowledge({
|