fuli-context 0.8.0 → 0.8.2
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 +4 -0
- package/README.zh-CN.md +4 -1
- package/dist/web/assets/{AboutPage-BjMmpwJB.js → AboutPage-fBq5VFoQ.js} +1 -1
- package/dist/web/assets/AgentHand-CMZk5GjY.js +1 -0
- package/dist/web/assets/AgentName-Cydd0UI4.css +1 -0
- package/dist/web/assets/AgentName-DRZHAIB2.js +1 -0
- package/dist/web/assets/AgentProfilePage-BPQEfMhB.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-CKZfrcuD.js +1 -0
- package/dist/web/assets/{ConnectionsPage-BKGxFDos.js → ConnectionsPage-COb56qnk.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-BLTpjCcK.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-CQwZnTA0.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-HUd-gne2.js +3 -0
- package/dist/web/assets/{GrowthLoading-WXCj8b0r.js → GrowthLoading-Bu_kH-I8.js} +1 -1
- package/dist/web/assets/{KnowledgeEditDialog-Bkie08ZA.js → KnowledgeEditDialog-BTwXXRCD.js} +1 -1
- package/dist/web/assets/{KnowledgeInspector-Cez-Z_95.js → KnowledgeInspector-Ct5hl2--.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-BYmTPA_8.js → KnowledgeOrganizerPage-DMC8Hh43.js} +1 -1
- package/dist/web/assets/KnowledgePage-uXW1Eovz.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-DKij2_B4.js → KnowledgeWorkspace-DmHIc0jb.js} +1 -1
- package/dist/web/assets/{NotFoundPage-HGV-q-bh.js → NotFoundPage-C0yOR9T9.js} +1 -1
- package/dist/web/assets/{OverviewPage-DE29_F_Y.js → OverviewPage-BreX_kb4.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-B95DFc7x.js → PersonalProfilePage-CX1a6_E3.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-Br1S8ETM.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-CA04h1Uf.js +13 -0
- package/dist/web/assets/ProjectAgentsPage-CZWWV4Kw.css +1 -0
- package/dist/web/assets/{ProjectScopePicker-Cu5PdxF6.js → ProjectScopePicker-DVHfXX1f.js} +1 -1
- package/dist/web/assets/{PublicProjectsPage-BY4B3_t3.js → PublicProjectsPage-CtctKml1.js} +1 -1
- package/dist/web/assets/{ReviewPage-C9vCesNu.js → ReviewPage-DUqaK-do.js} +1 -1
- package/dist/web/assets/{SearchableSelect-214TvfYg.js → SearchableSelect-BZtyYmZN.js} +1 -1
- package/dist/web/assets/{SettingsPage-D9TnzcFc.js → SettingsPage-CB4ymLr8.js} +1 -1
- package/dist/web/assets/{SharedJefaReportPage-CdSGI_OE.js → SharedJefaReportPage-CbFtrDCY.js} +1 -1
- package/dist/web/assets/{WritingTastePage-CUE1dU8f.js → WritingTastePage-B7lSzs2z.js} +1 -1
- package/dist/web/assets/agent-profile-CdKOh5xw.css +1 -0
- package/dist/web/assets/agent-profile.css_vue_type_style_index_0_src_true_lang-C22UdfU0.js +1 -0
- package/dist/web/assets/catalog-DU4Kfd-N.js +1 -0
- package/dist/web/assets/{client-Bl4oiW3E.js → client-CEHkJJ-5.js} +1 -1
- package/dist/web/assets/{console-BAIVNC-N.js → console--CZxc3PW.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DpRI_Jp2.js → conversation-launcher-settings-Co1RlTFH.js} +1 -1
- package/dist/web/assets/{i18n-CVFADb_M.js → i18n-BtOOoyJd.js} +3 -3
- package/dist/web/assets/{identity-Dt0WZgqo.js → identity-CsowvtIm.js} +1 -1
- package/dist/web/assets/index-7X3LGByg.js +2 -0
- package/dist/web/assets/index-CWMqDq8g.css +1 -0
- package/dist/web/assets/{model-B8sOxih0.js → model-CGpllrds.js} +1 -1
- package/dist/web/assets/{personal-profile-graph-aAqex7ty.js → personal-profile-graph-BNTG4PSY.js} +1 -1
- package/dist/web/assets/profile-model-BQO4zNs9.js +1 -0
- package/dist/web/assets/{shared-report-BQD6l_cS.js → shared-report-BFUJqqQe.js} +1 -1
- package/dist/web/assets/task-evidence-BUu7yYIn.js +1 -0
- package/dist/web/assets/{useApi-CROJJdhE-CuJ16v2S.js → useApi-CROJJdhE-raNlHRDl.js} +1 -1
- package/dist/web/assets/{useModalDialog-Ddv2XJQi.js → useModalDialog-CroGk2Cs.js} +1 -1
- package/dist/web/assets/{vue-router-C8KuEZ6a.js → vue-router-JFYBLXB1.js} +1 -1
- package/dist/web/index.html +12 -10
- package/docs/agent-conversations-and-collaboration.md +115 -0
- package/docs/agent-interface-architecture.md +17 -0
- package/docs/employee-agents.md +14 -1
- package/docs/project-agent-memory.md +34 -1
- package/graph-provider/fuli_graph/agent_conversation_models.py +72 -0
- package/graph-provider/fuli_graph/agent_conversation_routes.py +47 -0
- package/graph-provider/fuli_graph/agent_loan_models.py +28 -0
- package/graph-provider/fuli_graph/agent_quality_policy.py +151 -0
- package/graph-provider/fuli_graph/agent_verification_models.py +22 -0
- package/graph-provider/fuli_graph/conversation_context.py +30 -0
- package/graph-provider/fuli_graph/knowledge_search.py +8 -16
- package/graph-provider/fuli_graph/project_agent_executor_models.py +3 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +2 -0
- package/graph-provider/fuli_graph/project_agent_models.py +10 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +5 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +4 -0
- package/graph-provider/fuli_graph/runtime.py +14 -0
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_agent_conversations.py +206 -0
- package/graph-provider/fuli_graph/store_agent_loans.py +146 -0
- package/graph-provider/fuli_graph/store_agent_verification.py +111 -0
- package/graph-provider/fuli_graph/store_project_agent_executors.py +2 -0
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +6 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +20 -8
- package/graph-provider/fuli_graph/store_task_contexts.py +9 -0
- package/graph-provider/fuli_graph/task_context_routes.py +7 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +15 -15
- package/package.json +4 -3
- package/skills/capturing-session-knowledge/SKILL.md +20 -2
- package/skills/operating-fuli/SKILL.md +4 -0
- package/src/agent-tools/agent-collaboration-definitions.js +30 -0
- package/src/agent-tools/agent-conversation-definitions.js +21 -0
- package/src/agent-tools/employee-definitions.js +6 -5
- package/src/agent-tools/graph-definitions.js +19 -35
- package/src/agent-tools/graph-handlers.js +17 -1
- package/src/agent-tools/interface-catalog.js +2 -0
- package/src/agent-tools/project-agent-definitions.js +21 -0
- package/src/agent-tools/project-relation-definitions.js +36 -0
- package/src/agents/claude-code/conversation-transcript.js +20 -0
- package/src/agents/claude-code/lifecycle-hook.js +154 -0
- package/src/agents/claude-code/lifecycle-hooks.js +20 -0
- package/src/agents/codex/conversation-transcript.js +22 -0
- package/src/agents/codex/lifecycle-hook.js +47 -8
- package/src/agents/codex/lifecycle-hooks.js +6 -6
- package/src/agents/cursor/lifecycle-hook.js +5 -4
- package/src/app/agent-request-context.js +6 -1
- package/src/app/test-tools.js +26 -0
- package/src/conversations/sync-transcript.js +144 -0
- package/src/conversations/task-context-view.js +28 -0
- package/src/conversations/transcript-guard.js +54 -0
- package/src/conversations/transcript-reader.js +113 -0
- package/src/employees/service.js +8 -1
- package/src/graphiti/agent-collaboration.js +62 -0
- package/src/graphiti/agent-conversations.js +75 -0
- package/src/graphiti/agent-knowledge-workflows.js +8 -2
- package/src/graphiti/project-agent-control-plane.js +22 -2
- package/src/graphiti/project-agent-mapping.js +19 -1
- package/src/graphiti/project-agent-task-entry.js +11 -1
- package/src/graphiti/project-agent-workflows.js +11 -1
- package/src/graphiti/project-path-context.js +17 -10
- package/src/graphiti/provider-client.js +17 -0
- package/src/graphiti/verification-preference.js +24 -0
- package/src/http/graph-api-router.js +17 -0
- package/src/mcp/create-mcp-server.js +19 -5
- package/src/mcp/employee-tool-catalog.js +22 -0
- package/src/mcp/session-id.js +4 -0
- package/src/mcp/tool-annotations.js +8 -1
- package/src/mcp/tool-result.js +3 -1
- package/src/setup/agent-installation-status.js +1 -0
- package/src/setup/claude-code-config.js +13 -19
- package/dist/web/assets/AgentHand-BfLYPGjg.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-D-W043v4.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-Cz7y-IFD.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-HHQPxdqI.js +0 -3
- package/dist/web/assets/KnowledgePage-Ck7tIBDn.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-Bt7tFF5Z.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-BioDR9OW.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-CiiPNe_G.js +0 -12
- package/dist/web/assets/index-BUMq5zNh.js +0 -2
- package/dist/web/assets/index-C0ci8dnU.css +0 -1
- package/src/mcp/task-context-registry.js +0 -165
|
@@ -3,6 +3,7 @@ import { booleanSchema, objectSchema, stringSchema } from './schema.js';
|
|
|
3
3
|
const id = { ...stringSchema(), minLength: 1, maxLength: 256 };
|
|
4
4
|
const projectPath = { ...stringSchema(), minLength: 1, maxLength: 4096 };
|
|
5
5
|
const target = { projectPath, templateId: id };
|
|
6
|
+
const workbenchTarget = { ...target, personalProjectId: id };
|
|
6
7
|
|
|
7
8
|
export const EMPLOYEE_TOOL_DEFINITIONS = [
|
|
8
9
|
{
|
|
@@ -29,15 +30,15 @@ export const EMPLOYEE_TOOL_DEFINITIONS = [
|
|
|
29
30
|
},
|
|
30
31
|
{
|
|
31
32
|
name: 'list_employee_tools', title: 'READ · Employee workbench tools',
|
|
32
|
-
description: 'Discover
|
|
33
|
-
inputSchema: objectSchema(
|
|
33
|
+
description: 'Discover complete input schemas and permitted tools for a recruited employee. Supply projectPath, or a previously resolved personalProjectId without projectPath when directory resolution is unavailable. If both are supplied they must resolve to the same project. Existing project and employee authorization always apply. Use includeSchemas=false for a compact name list, then toolName to read one exact schema. Oversized contracts fail explicitly, never return partial schemas. Read the schema before calling; a workbench has to be installed first.',
|
|
34
|
+
inputSchema: objectSchema({ ...workbenchTarget, toolName: id, includeSchemas: booleanSchema() }, ['templateId'])
|
|
34
35
|
},
|
|
35
36
|
{
|
|
36
37
|
name: 'call_employee_tool', title: 'WRITE · Call an employee workbench tool',
|
|
37
|
-
description: 'Call a discovered employee tool inside the exact assigned project. May read or change data according to the tool schema. Preserve optimistic concurrency and idempotency keys. Does not grant cross-project access, execute an external model, or confirm task completion for the human.',
|
|
38
|
+
description: 'Call a discovered employee tool inside the exact assigned project. Supply projectPath or a previously resolved personalProjectId; omit projectPath if directory resolution is unavailable. If both are supplied they must resolve to the same project. May read or change data according to the tool schema. Preserve optimistic concurrency and idempotency keys. Does not grant cross-project access, execute an external model, or confirm task completion for the human.',
|
|
38
39
|
inputSchema: objectSchema({
|
|
39
|
-
...
|
|
40
|
+
...workbenchTarget, tool: id,
|
|
40
41
|
arguments: { type: 'object', additionalProperties: true }
|
|
41
|
-
}, ['
|
|
42
|
+
}, ['templateId', 'tool', 'arguments'])
|
|
42
43
|
}
|
|
43
44
|
];
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { projectRelationDefinitions } from './project-relation-definitions.js';
|
|
2
|
+
import { AGENT_COLLABORATION_DEFINITIONS } from './agent-collaboration-definitions.js';
|
|
3
|
+
import { AGENT_CONVERSATION_DEFINITIONS } from './agent-conversation-definitions.js';
|
|
1
4
|
import {
|
|
2
5
|
arraySchema,
|
|
3
6
|
booleanSchema,
|
|
@@ -670,6 +673,10 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
670
673
|
description: 'Resolve the exact local project from projectPath and return its lightweight Agent directory. Optionally filter active/inactive/archived records or find Agents whose capabilities or responsibility match a capability query. This never loads another Agent\'s private memory.',
|
|
671
674
|
inputSchema: objectSchema({
|
|
672
675
|
projectPath: boundedString(4096),
|
|
676
|
+
personalProjectId: {
|
|
677
|
+
...nullableStringSchema(), minLength: 1, maxLength: 128,
|
|
678
|
+
description: 'Explicit existing project ID when the directory has no exact match. A conflicting exact path match is rejected.'
|
|
679
|
+
},
|
|
673
680
|
status: nullableProjectAgentStatus,
|
|
674
681
|
capability: nullableStringSchema()
|
|
675
682
|
}, ['projectPath'])
|
|
@@ -680,6 +687,10 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
680
687
|
description: 'Resolve one active project Agent and assemble only the context needed for the task: personal-global rules, exact and authorized inherited project knowledge, this Agent\'s preferences and memory, and focused query results. It excludes every other project Agent. Fuli remains a control layer; the calling host Agent performs reasoning and execution.',
|
|
681
688
|
inputSchema: objectSchema({
|
|
682
689
|
projectPath: boundedString(4096),
|
|
690
|
+
personalProjectId: {
|
|
691
|
+
...nullableStringSchema(), minLength: 1, maxLength: 128,
|
|
692
|
+
description: 'Explicit existing project ID when the directory has no exact match. A conflicting exact path match is rejected.'
|
|
693
|
+
},
|
|
683
694
|
agentId: boundedString(128),
|
|
684
695
|
queries: arraySchema(shortText, { minItems: 1, maxItems: 10 }),
|
|
685
696
|
limitPerQuery: integerSchema({ minimum: 1, maximum: 50 }),
|
|
@@ -697,6 +708,8 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
697
708
|
}, ['personalSpaceId', 'agentId'])
|
|
698
709
|
},
|
|
699
710
|
...PROJECT_AGENT_MEMORY_DEFINITIONS,
|
|
711
|
+
...AGENT_CONVERSATION_DEFINITIONS,
|
|
712
|
+
...AGENT_COLLABORATION_DEFINITIONS,
|
|
700
713
|
{
|
|
701
714
|
name: 'delete_project_agent',
|
|
702
715
|
title: 'WRITE · Archive one Agent identity',
|
|
@@ -710,7 +723,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
710
723
|
{
|
|
711
724
|
name: 'cleanup_test_project_agents',
|
|
712
725
|
title: 'WRITE · Archive test Agent roles',
|
|
713
|
-
description: 'Archive only test-marked Project Agents for one explicit test source through the Provider. Durable production identities are not touched; the result reports the number archived for cleanup verification.',
|
|
726
|
+
description: 'TEST-ONLY. Archive only test-marked Project Agents for one explicit test source through the Provider. Disabled on default MCP/HTTP surfaces unless FULI_ENABLE_TEST_TOOLS=1. Durable production identities are not touched; the result reports the number archived for cleanup verification.',
|
|
714
727
|
inputSchema: objectSchema({
|
|
715
728
|
personalSpaceId: id,
|
|
716
729
|
testSource: boundedString(256)
|
|
@@ -748,7 +761,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
748
761
|
{
|
|
749
762
|
name: 'coordinate_project_agent_task',
|
|
750
763
|
title: 'WRITE · Prepare a host-executed Agent team',
|
|
751
|
-
description: 'Resolve the exact local project, let the Provider coordinator route one durable task, and assemble an isolated context bundle for every selected lead/collaborator. Pass the current taskContextToken so a newly recruited lead can claim an initially unassigned task, including its blocked work. The result is a truthful host execution plan: Fuli persists identity, staffing, and memory scope, while the calling host must start real workers, report their concrete worker/executor evidence, and finish their lifecycle. This tool never claims that a selected Agent is online or that a worker was started.',
|
|
764
|
+
description: 'Resolve the exact local project, let the Provider coordinator route one durable task, and assemble an isolated context bundle for every selected lead/collaborator. Pass the current taskContextToken so a newly recruited lead can claim an initially unassigned task, including its blocked work. When recruiting, HR should set recruitmentProfile.character from the role responsibilities and preserve user expectations; configured traits must never be presented as proven growth. The result is a truthful host execution plan: Fuli persists identity, staffing, and memory scope, while the calling host must start real workers, report their concrete worker/executor evidence, and finish their lifecycle. This tool never claims that a selected Agent is online or that a worker was started.',
|
|
752
765
|
inputSchema: projectAgentTaskCoordinateInput
|
|
753
766
|
},
|
|
754
767
|
{
|
|
@@ -776,7 +789,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
776
789
|
{
|
|
777
790
|
name: 'submit_project_agent_task',
|
|
778
791
|
title: 'WRITE · Submit a routed Agent task',
|
|
779
|
-
description: 'Submit one durable task to the FULI control plane. Existing matching durable Agents are preferred; no anonymous temporary Agent is created. The Agent locked executor policy outranks task, assignment, project, and space rules; explicit user priority is never lowered for token savings. The space coordinator, complexity, model strategy, routing decision, boundaries, actual executor/model/rule/fallback audit fields, and any HR disclosure are persisted by the Provider.',
|
|
792
|
+
description: 'Submit one durable task to the FULI control plane. Every new task requires a passed quality gate for its exact artifact revision before completion; use record_agent_verification with actual run evidence. Existing matching durable Agents are preferred; no anonymous temporary Agent is created. The Agent locked executor policy outranks task, assignment, project, and space rules; explicit user priority is never lowered for token savings. The space coordinator, complexity, model strategy, routing decision, boundaries, actual executor/model/rule/fallback audit fields, and any HR disclosure are persisted by the Provider. HR should fill recruitmentProfile.character from role responsibilities, preserve user expectations, and distinguish configured traits from demonstrated growth; never invent past achievements.',
|
|
780
793
|
inputSchema: projectAgentTaskSubmitInput
|
|
781
794
|
},
|
|
782
795
|
{
|
|
@@ -793,7 +806,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
793
806
|
{
|
|
794
807
|
name: 'record_project_agent_task_activity',
|
|
795
808
|
title: 'WRITE · Record task activity',
|
|
796
|
-
description: 'Record
|
|
809
|
+
description: 'Record real task/worker activity and optional source-labelled cumulative token usage; never estimate. workerStatus requires workerId and agentId. Report actualExecutorId, actualModelProvider and actualModel together only with matching authorized preflight evidence. Otherwise omit all three, report observed workerRuntime and explain the mismatch in summary. A terminal activity with valid actual execution automatically records its routing outcome.',
|
|
797
810
|
inputSchema: projectAgentTaskActivityInput
|
|
798
811
|
},
|
|
799
812
|
{
|
|
@@ -1003,7 +1016,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
1003
1016
|
{
|
|
1004
1017
|
name: 'record_project_agent_task_outcome',
|
|
1005
1018
|
title: 'WRITE · Record explicit routing outcome evidence',
|
|
1006
|
-
description: 'Record
|
|
1019
|
+
description: 'Record explicit user/test outcome evidence against a matching recorded execution. Use the task workKind and effective modelStrategy plus real referenceIds; a CLI session ID alone is not a recorded run. Terminal routing outcomes are already derived from valid task activity; do not submit them again. Never infer satisfaction or fabricate execution evidence to clear a validation error.',
|
|
1007
1020
|
inputSchema: projectAgentTaskOutcomeInput
|
|
1008
1021
|
},
|
|
1009
1022
|
{
|
|
@@ -1194,36 +1207,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
1194
1207
|
providerUrl: shortText
|
|
1195
1208
|
}, ['projectId', 'providerUrl'])
|
|
1196
1209
|
},
|
|
1197
|
-
{
|
|
1198
|
-
name: 'create_project_relation',
|
|
1199
|
-
description: 'Create a canonical relationship between two public projects. PART_OF waits for target/parent Maintainer confirmation; other relation types become active immediately. This never subscribes either project.',
|
|
1200
|
-
inputSchema: objectSchema({
|
|
1201
|
-
sourceProjectId: id,
|
|
1202
|
-
targetProjectId: id,
|
|
1203
|
-
providerUrl: shortText,
|
|
1204
|
-
relationType: projectRelationType,
|
|
1205
|
-
note: nullableStringSchema()
|
|
1206
|
-
}, ['sourceProjectId', 'targetProjectId', 'providerUrl', 'relationType'])
|
|
1207
|
-
},
|
|
1208
|
-
{
|
|
1209
|
-
name: 'list_project_relations',
|
|
1210
|
-
description: 'List canonical incoming and outgoing project relationships. Related projects are suggestions only and never expand the active subscription set.',
|
|
1211
|
-
inputSchema: objectSchema({
|
|
1212
|
-
projectId: id,
|
|
1213
|
-
providerUrl: shortText
|
|
1214
|
-
}, ['projectId', 'providerUrl'])
|
|
1215
|
-
},
|
|
1216
|
-
{
|
|
1217
|
-
name: 'review_project_relation',
|
|
1218
|
-
description: 'Confirm or reject a pending PART_OF relationship as a Maintainer of the target parent project.',
|
|
1219
|
-
inputSchema: objectSchema({
|
|
1220
|
-
targetProjectId: id,
|
|
1221
|
-
relationId: id,
|
|
1222
|
-
providerUrl: shortText,
|
|
1223
|
-
decision: enumSchema(['confirm', 'reject']),
|
|
1224
|
-
note: nullableStringSchema()
|
|
1225
|
-
}, ['targetProjectId', 'relationId', 'providerUrl', 'decision'])
|
|
1226
|
-
},
|
|
1210
|
+
...projectRelationDefinitions({ id, shortText, projectRelationType }),
|
|
1227
1211
|
{
|
|
1228
1212
|
name: 'list_personal_review_queue',
|
|
1229
1213
|
description: 'List full structured knowledge drafts waiting in the local personal pre-review queue before any public submission.',
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { ApplicationError } from '../app/application-error.js';
|
|
2
|
+
import { assertTestToolsEnabled } from '../app/test-tools.js';
|
|
3
|
+
import { activeTestToolsEnabled } from '../app/agent-request-context.js';
|
|
2
4
|
import { agentInterfaceCatalog } from './interface-catalog.js';
|
|
3
5
|
|
|
4
6
|
const HANDLERS = Object.freeze({
|
|
7
|
+
get_agent_quality_gate: (app, input) => app.agentVerification('query', input),
|
|
8
|
+
record_agent_verification: (app, input) => app.agentVerification('record', input),
|
|
9
|
+
plan_agent_collaboration: (app, input) => app.planAgentCollaboration(input),
|
|
10
|
+
request_agent_loan: (app, input) => app.agentLoan('request', input),
|
|
11
|
+
decide_agent_loan: (app, input) => app.agentLoan('decide', input),
|
|
12
|
+
list_agent_loans: (app, input) => app.agentLoan('query', input),
|
|
13
|
+
list_agent_conversations: (app, input) => app.queryAgentConversations({ ...input, mode: 'list' }),
|
|
14
|
+
read_agent_conversation: (app, input) => app.queryAgentConversations({ ...input, mode: 'events' }),
|
|
15
|
+
resume_agent_conversation: (app, input) => app.resumeAgentConversation(input),
|
|
16
|
+
get_agent_conversation_policy: (app, input) => app.queryAgentConversations({ ...input, mode: 'policy' }),
|
|
17
|
+
update_agent_conversation_policy: (app, input) => app.updateAgentConversationPolicy(input),
|
|
5
18
|
list_project_agent_tasks: (app, input) => app.listProjectAgentTasks(input),
|
|
6
19
|
request_agent_attention: (app, input) => app.requestAgentAttention(input),
|
|
7
20
|
list_agent_attention: (app, input) => app.listAgentAttention(input),
|
|
@@ -107,7 +120,10 @@ const HANDLERS = Object.freeze({
|
|
|
107
120
|
list_project_agents: (app, input) => app.listCurrentProjectAgents(input),
|
|
108
121
|
get_project_agent: (app, input) => app.getProjectAgent(input),
|
|
109
122
|
delete_project_agent: (app, input) => app.deleteProjectAgent(input),
|
|
110
|
-
cleanup_test_project_agents: (app, input) =>
|
|
123
|
+
cleanup_test_project_agents: (app, input) => {
|
|
124
|
+
assertTestToolsEnabled({ FULI_ENABLE_TEST_TOOLS: activeTestToolsEnabled() ? '1' : '0' });
|
|
125
|
+
return app.cleanupProjectAgentTestRoles(input);
|
|
126
|
+
},
|
|
111
127
|
create_project_agent_assignment: (app, input) =>
|
|
112
128
|
app.createProjectAgentAssignment(input),
|
|
113
129
|
list_project_agent_assignments: (app, input) =>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const UI_MUTATION_PARITY = Object.freeze([
|
|
2
|
+
agent('POST /api/agent-conversations/query', 'list_agent_conversations', 'read_agent_conversation', 'get_agent_conversation_policy'),
|
|
3
|
+
agent('PUT /api/agent-conversations/policy', 'update_agent_conversation_policy'),
|
|
2
4
|
agent('POST /api/agent-attention', 'request_agent_attention'),
|
|
3
5
|
agent('POST /api/agent-attention/cancel', 'cancel_agent_attention'),
|
|
4
6
|
local('POST /api/agent-attention/respond', 'Only a local person may respond to an Agent request. A response records a decision, but never executes permissions or task acceptance.'),
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
enumSchema,
|
|
5
5
|
integerSchema,
|
|
6
6
|
nullableStringSchema,
|
|
7
|
+
numberSchema,
|
|
7
8
|
objectSchema,
|
|
8
9
|
stringSchema
|
|
9
10
|
} from './schema.js';
|
|
@@ -54,6 +55,18 @@ export const projectAgentProfile = objectSchema({
|
|
|
54
55
|
name: boundedString(160),
|
|
55
56
|
displayName: boundedString(160),
|
|
56
57
|
responsibility: boundedString(4096),
|
|
58
|
+
character: {
|
|
59
|
+
...objectSchema({
|
|
60
|
+
judgment: { type: 'string', maxLength: 2048, default: '' },
|
|
61
|
+
taste: { type: 'string', maxLength: 2048, default: '' },
|
|
62
|
+
personality: { type: 'string', maxLength: 2048, default: '' }
|
|
63
|
+
}),
|
|
64
|
+
description: 'HR-configured judgment, taste and personality based on role responsibilities. These are intended working traits, not evidence of observed growth or performance.'
|
|
65
|
+
},
|
|
66
|
+
expectations: {
|
|
67
|
+
type: 'string', maxLength: 4096, default: '',
|
|
68
|
+
description: 'User expectations for this Agent. Preserve the user’s wording and leave empty when none were provided.'
|
|
69
|
+
},
|
|
57
70
|
occupationEmoji: { ...nullableStringSchema(), minLength: 1, maxLength: 32 },
|
|
58
71
|
agentType: enumSchema(['coordinator', 'durable', 'hr', 'temporary']),
|
|
59
72
|
workKinds: arraySchema(boundedString(512), { maxItems: 32 }),
|
|
@@ -156,6 +169,10 @@ export const projectAgentTaskSubmitInput = objectSchema({
|
|
|
156
169
|
export const projectAgentTaskCoordinateInput = objectSchema({
|
|
157
170
|
taskContextToken: id,
|
|
158
171
|
projectPath: boundedString(4096),
|
|
172
|
+
personalProjectId: {
|
|
173
|
+
...nullableStringSchema(), minLength: 1, maxLength: 128,
|
|
174
|
+
description: 'Explicit existing project ID when the directory has no exact match. A conflicting exact path match is rejected.'
|
|
175
|
+
},
|
|
159
176
|
idempotencyKey,
|
|
160
177
|
title: boundedString(160),
|
|
161
178
|
objective: boundedString(4096),
|
|
@@ -181,6 +198,7 @@ export const projectAgentTaskCoordinateInput = objectSchema({
|
|
|
181
198
|
'routingReason', 'contextQueries'
|
|
182
199
|
]);
|
|
183
200
|
export const projectAgentTaskActivityInput = objectSchema({
|
|
201
|
+
artifactRevision: { ...stringSchema(), minLength: 1, maxLength: 160 },
|
|
184
202
|
personalSpaceId: id,
|
|
185
203
|
personalProjectId: id,
|
|
186
204
|
taskId: id,
|
|
@@ -235,6 +253,8 @@ const executorActualModelStrategySource = enumSchema([
|
|
|
235
253
|
'task', 'assignment', 'agent', 'routing_rule', 'coordinator'
|
|
236
254
|
]);
|
|
237
255
|
export const executorModel = objectSchema({
|
|
256
|
+
capabilityTier: integerSchema({ minimum: 1, maximum: 3 }),
|
|
257
|
+
costRank: numberSchema({ minimum: 0 }),
|
|
238
258
|
provider: boundedString(128),
|
|
239
259
|
model: boundedString(256),
|
|
240
260
|
capabilities: arraySchema(boundedString(512), { maxItems: 32 }),
|
|
@@ -276,6 +296,7 @@ export const executorRoutingRuleInput = objectSchema({
|
|
|
276
296
|
idempotencyKey
|
|
277
297
|
}, ['scope', 'workKind', 'reason', 'idempotencyKey']);
|
|
278
298
|
export const executorActualReportInput = objectSchema({
|
|
299
|
+
artifactRevision: { ...stringSchema(), minLength: 1, maxLength: 160 },
|
|
279
300
|
personalSpaceId: id,
|
|
280
301
|
personalProjectId: id,
|
|
281
302
|
taskId: id,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { enumSchema, nullableStringSchema, objectSchema } from './schema.js';
|
|
2
|
+
|
|
3
|
+
export function projectRelationDefinitions({ id, shortText, projectRelationType }) {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
name: 'create_project_relation',
|
|
7
|
+
description: 'Create a canonical relationship between two public projects. PART_OF waits for target/parent Maintainer confirmation; other relation types become active immediately. This never subscribes either project.',
|
|
8
|
+
inputSchema: objectSchema({
|
|
9
|
+
sourceProjectId: id,
|
|
10
|
+
targetProjectId: id,
|
|
11
|
+
providerUrl: shortText,
|
|
12
|
+
relationType: projectRelationType,
|
|
13
|
+
note: nullableStringSchema()
|
|
14
|
+
}, ['sourceProjectId', 'targetProjectId', 'providerUrl', 'relationType'])
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'list_project_relations',
|
|
18
|
+
description: 'List canonical incoming and outgoing project relationships. Related projects are suggestions only and never expand the active subscription set.',
|
|
19
|
+
inputSchema: objectSchema({
|
|
20
|
+
projectId: id,
|
|
21
|
+
providerUrl: shortText
|
|
22
|
+
}, ['projectId', 'providerUrl'])
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'review_project_relation',
|
|
26
|
+
description: 'Confirm or reject a pending PART_OF relationship as a Maintainer of the target parent project.',
|
|
27
|
+
inputSchema: objectSchema({
|
|
28
|
+
targetProjectId: id,
|
|
29
|
+
relationId: id,
|
|
30
|
+
providerUrl: shortText,
|
|
31
|
+
decision: enumSchema(['confirm', 'reject']),
|
|
32
|
+
note: nullableStringSchema()
|
|
33
|
+
}, ['targetProjectId', 'relationId', 'providerUrl', 'decision'])
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Claude JSONL v1; thinking/redacted_thinking are intentionally never persisted.
|
|
2
|
+
export function normalizeClaudeRecord(record, sessionId) {
|
|
3
|
+
if (record?.sessionId !== sessionId || !['user', 'assistant'].includes(record.type)) return [];
|
|
4
|
+
const content = record.message?.content;
|
|
5
|
+
if (typeof content === 'string') return content ? [{ role: record.type, kind: 'message', content }] : [];
|
|
6
|
+
if (!Array.isArray(content)) return [];
|
|
7
|
+
return content.flatMap(block => {
|
|
8
|
+
if (block.type === 'text' && block.text) return [{ role: record.type, kind: 'message', content: block.text }];
|
|
9
|
+
if (block.type === 'tool_use') return [{ role: 'assistant', kind: 'tool_call', content: JSON.stringify({
|
|
10
|
+
call_id: block.id, name: block.name, input: block.input
|
|
11
|
+
}) }];
|
|
12
|
+
if (block.type === 'tool_result') return [{ role: 'tool', kind: 'tool_result', content: JSON.stringify({
|
|
13
|
+
call_id: block.tool_use_id, output: block.content, is_error: block.is_error ?? false
|
|
14
|
+
}) }];
|
|
15
|
+
return [];
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function verifyClaudeTranscript(firstRecord, sessionId) {
|
|
19
|
+
return firstRecord?.sessionId === sessionId;
|
|
20
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { withTranscriptGuard } from '../../conversations/transcript-guard.js';
|
|
3
|
+
import { compactTaskContext } from '../../conversations/task-context-view.js';
|
|
4
|
+
import { syncConversationTranscript, withTranscriptNotice, claimTranscriptBoundary, recordTranscriptTaskEntry } from '../../conversations/sync-transcript.js';
|
|
5
|
+
import { normalizeClaudeRecord, verifyClaudeTranscript } from './conversation-transcript.js';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { callAgentTool } from '../../agent-tools.js';
|
|
10
|
+
import { createRuntimeLeaseClient } from '../../adaptive-runtime/lease-client.js';
|
|
11
|
+
import { openFederatedGraphApplication } from '../../graphiti/federated-application.js';
|
|
12
|
+
import { resolveGraphRuntimeOptions } from '../../graphiti/runtime-config.js';
|
|
13
|
+
import { hookAdditionalContextToolResult } from '../../mcp/tool-result.js';
|
|
14
|
+
import { boundedHookMessage } from '../lifecycle-message.js';
|
|
15
|
+
|
|
16
|
+
export async function claudeLifecycleOutput(event, input, invoke) {
|
|
17
|
+
if (!['UserPromptSubmit', 'Stop'].includes(event)) throw new TypeError('Unsupported Claude lifecycle event');
|
|
18
|
+
const sessionId = input?.session_id;
|
|
19
|
+
if (typeof sessionId !== 'string' || !sessionId.trim() || sessionId.length > 256) {
|
|
20
|
+
throw new TypeError('Claude lifecycle requires a host session identifier');
|
|
21
|
+
}
|
|
22
|
+
const source = { sourceApplication: 'claude_code', sourceSessionId: sessionId };
|
|
23
|
+
if (event === 'UserPromptSubmit') {
|
|
24
|
+
if (typeof input.cwd !== 'string' || !input.cwd.trim() || typeof input.prompt !== 'string') {
|
|
25
|
+
throw new TypeError('Claude task entry requires cwd and prompt');
|
|
26
|
+
}
|
|
27
|
+
const context = await invoke('begin_task_context', {
|
|
28
|
+
sessionId, projectPath: input.cwd, taskPrompt: input.prompt.slice(0, 8192), ...source
|
|
29
|
+
});
|
|
30
|
+
return JSON.parse(hookAdditionalContextToolResult(compactTaskContext(context), {
|
|
31
|
+
hookEventName: event,
|
|
32
|
+
label: 'Fuli task context. Apply effective_preferences and use taskContextToken for the final checkpoint.',
|
|
33
|
+
limitBytes: 128 * 1024,
|
|
34
|
+
itemLimit: 1000
|
|
35
|
+
}).content[0].text);
|
|
36
|
+
}
|
|
37
|
+
const check = await invoke('verify_task_checkpoint', { sessionId, ...source });
|
|
38
|
+
const reason = boundedHookMessage(check?.reason);
|
|
39
|
+
if (check?.decision !== 'block' || !reason.trim()) return {};
|
|
40
|
+
if (input.stop_hook_active === true) {
|
|
41
|
+
if (typeof check.task_context_token === 'string' && check.task_context_token.trim()) {
|
|
42
|
+
await invoke('checkpoint_task_knowledge', {
|
|
43
|
+
taskContextToken: check.task_context_token,
|
|
44
|
+
disposition: 'retain_nothing',
|
|
45
|
+
reason: 'Claude Code Stop hook fallback after one checkpoint continuation.',
|
|
46
|
+
workLog: { status: 'incomplete', summary: 'The host stopped before the employee submitted a work summary. Review the prior task before resuming.' },
|
|
47
|
+
...source
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
return { decision: 'block', reason };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function claudeLifecycleUnavailable(event) {
|
|
56
|
+
const warning = 'Fuli context is unavailable; memory recovery and task checkpoint are not verified. '
|
|
57
|
+
+ 'Continue with the user request and use Fuli MCP when it becomes available.';
|
|
58
|
+
return event === 'UserPromptSubmit'
|
|
59
|
+
? { systemMessage: warning, hookSpecificOutput: { hookEventName: event, additionalContext: warning } }
|
|
60
|
+
: { systemMessage: warning };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runClaudeLifecycleHook(args, dependencies = {}) {
|
|
64
|
+
const event = args[args.indexOf('--event') + 1];
|
|
65
|
+
const input = await (dependencies.readInput ?? readHookInput)();
|
|
66
|
+
if (dependencies.invoke) return claudeLifecycleOutput(event, input, dependencies.invoke);
|
|
67
|
+
const { runtimeConfigPath } = (dependencies.resolveRuntimeOptions ?? resolveGraphRuntimeOptions)(
|
|
68
|
+
args, dependencies.env ?? process.env
|
|
69
|
+
);
|
|
70
|
+
let app;
|
|
71
|
+
let leases;
|
|
72
|
+
try {
|
|
73
|
+
app = (dependencies.openApplication ?? openFederatedGraphApplication)({ runtimeConfigPath });
|
|
74
|
+
leases = (dependencies.createLeases ?? createRuntimeLeaseClient)({ runtimeConfigPath });
|
|
75
|
+
const request = { signal: dependencies.signal };
|
|
76
|
+
return await leases.withGraphLease('claude-code-lifecycle', async () => {
|
|
77
|
+
try {
|
|
78
|
+
if (app.getCapturePolicy?.().enabled && input.transcript_path) dependencies.onTranscriptGuard?.();
|
|
79
|
+
return await withTranscriptGuard(runtimeConfigPath, input, 'claude_code', app.getCapturePolicy?.().enabled, async guard => {
|
|
80
|
+
let sync = await syncConversationTranscript(app, input, 'claude_code', {
|
|
81
|
+
normalize: normalizeClaudeRecord, verify: verifyClaudeTranscript
|
|
82
|
+
}, guard);
|
|
83
|
+
if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
|
|
84
|
+
const entryGuard = sync.status === 'capture_disabled' ? null : guard;
|
|
85
|
+
if (event === 'UserPromptSubmit' && entryGuard?.value) await entryGuard.write({ ...entryGuard.value, phase: 'beginning' });
|
|
86
|
+
const output = await claudeLifecycleOutput(event, input, async (name, parameters) => {
|
|
87
|
+
const context = await (dependencies.callTool ?? callAgentTool)(app, name, parameters, request);
|
|
88
|
+
if (name === 'begin_task_context') await recordTranscriptTaskEntry(entryGuard, context);
|
|
89
|
+
return context;
|
|
90
|
+
});
|
|
91
|
+
if (event === 'UserPromptSubmit') sync = await claimTranscriptBoundary(app, input, 'claude_code', sync, {
|
|
92
|
+
normalize: normalizeClaudeRecord, verify: verifyClaudeTranscript
|
|
93
|
+
}, entryGuard);
|
|
94
|
+
if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
|
|
95
|
+
return withTranscriptNotice(output, sync);
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (event !== 'UserPromptSubmit' || !app.getCapturePolicy?.().enabled || !input.transcript_path) throw error;
|
|
99
|
+
return { decision: 'block', reason: error.code === 'EADDRINUSE'
|
|
100
|
+
? 'The session handoff lock is occupied. Retry after the current hook finishes; if it persists, start a new host session and resume the intended Agent.'
|
|
101
|
+
: 'Fuli transcript handoff could not be completed safely. Retry task entry; no previous Agent context was supplied.' };
|
|
102
|
+
}
|
|
103
|
+
}, request);
|
|
104
|
+
} finally {
|
|
105
|
+
await Promise.allSettled([close(leases), close(app)]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function close(resource) { await resource?.close(); }
|
|
110
|
+
|
|
111
|
+
async function readHookInput() {
|
|
112
|
+
const chunks = [];
|
|
113
|
+
let size = 0;
|
|
114
|
+
for await (const chunk of process.stdin) {
|
|
115
|
+
size += chunk.length;
|
|
116
|
+
if (size > 256 * 1024) throw new TypeError('Hook input exceeds limit');
|
|
117
|
+
chunks.push(chunk);
|
|
118
|
+
}
|
|
119
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
const args = process.argv.slice(2);
|
|
124
|
+
const event = args[args.indexOf('--event') + 1];
|
|
125
|
+
const configured = args.includes('--timeout-ms') ? Number(args[args.indexOf('--timeout-ms') + 1]) : 28000;
|
|
126
|
+
const timeoutMs = Number.isSafeInteger(configured) && configured > 0 && configured <= 2_147_483_647
|
|
127
|
+
? configured : 28000;
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
let emitted = false;
|
|
130
|
+
let guarded = false;
|
|
131
|
+
const emit = (output, callback) => {
|
|
132
|
+
if (emitted) return;
|
|
133
|
+
emitted = true;
|
|
134
|
+
process.stdout.write(`${JSON.stringify(output)}\n`, callback);
|
|
135
|
+
};
|
|
136
|
+
// Finish before the host timeout, even if a dependency ignores cancellation.
|
|
137
|
+
// No prompt, transcript, path or raw exception is written to diagnostics.
|
|
138
|
+
const deadline = setTimeout(() => {
|
|
139
|
+
controller.abort();
|
|
140
|
+
emit(guarded && event === 'UserPromptSubmit' ? { decision: 'block', reason: 'Fuli transcript handoff timed out. Retry task entry; the previous Agent context was not supplied.' } : claudeLifecycleUnavailable(event), () => process.exit(0));
|
|
141
|
+
}, timeoutMs);
|
|
142
|
+
const abort = setTimeout(() => controller.abort(), Math.max(1, timeoutMs - 1500));
|
|
143
|
+
try {
|
|
144
|
+
const output = await runClaudeLifecycleHook(args, { signal: controller.signal, onTranscriptGuard: () => { guarded = true; } });
|
|
145
|
+
emit(output);
|
|
146
|
+
} catch {
|
|
147
|
+
emit(claudeLifecycleUnavailable(event));
|
|
148
|
+
} finally {
|
|
149
|
+
clearTimeout(abort);
|
|
150
|
+
clearTimeout(deadline);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { quoteShellArgument } from '../../cli/shell-argument.js';
|
|
3
|
+
|
|
4
|
+
export const CLAUDE_LIFECYCLE_MARKER = '--fuli-claude-lifecycle';
|
|
5
|
+
|
|
6
|
+
export function claudeLifecycleCommand(context, event, timeoutSec) {
|
|
7
|
+
const script = context.claudeLifecycleHookPath ??
|
|
8
|
+
join(dirname(context.mcpServerPath), 'agents', 'claude-code', 'lifecycle-hook.js');
|
|
9
|
+
return [
|
|
10
|
+
context.nodePath, script, CLAUDE_LIFECYCLE_MARKER,
|
|
11
|
+
'--runtime-config', context.runtimeConfigPath, '--event', event,
|
|
12
|
+
'--timeout-ms', String(Math.max(100, timeoutSec * 1000 - 2000))
|
|
13
|
+
].map(value => quoteShellArgument(value, context.platform)).join(' ');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isClaudeLifecycleCommand(hook) {
|
|
17
|
+
return hook?.type === 'command' && typeof hook.command === 'string' &&
|
|
18
|
+
hook.command.includes(CLAUDE_LIFECYCLE_MARKER) &&
|
|
19
|
+
hook.command.includes('lifecycle-hook.js');
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Codex rollout v1: visible response items only; event_msg mirrors are omitted.
|
|
2
|
+
export function normalizeCodexRecord(record) {
|
|
3
|
+
if (record?.type !== 'response_item') return [];
|
|
4
|
+
const item = record.payload;
|
|
5
|
+
if (item?.type === 'message' && ['user', 'assistant'].includes(item.role)) {
|
|
6
|
+
const content = (item.content ?? []).filter(block => ['input_text', 'output_text'].includes(block.type))
|
|
7
|
+
.map(block => block.text ?? '').join('\n');
|
|
8
|
+
return content ? [{ role: item.role, kind: 'message', content }] : [];
|
|
9
|
+
}
|
|
10
|
+
if (['function_call', 'custom_tool_call'].includes(item?.type)) {
|
|
11
|
+
return [{ role: 'assistant', kind: 'tool_call', content: JSON.stringify({
|
|
12
|
+
call_id: item.call_id, name: item.name, arguments: item.arguments ?? item.input
|
|
13
|
+
}) }];
|
|
14
|
+
}
|
|
15
|
+
if (['function_call_output', 'custom_tool_call_output'].includes(item?.type)) {
|
|
16
|
+
return [{ role: 'tool', kind: 'tool_result', content: JSON.stringify({ call_id: item.call_id, output: item.output }) }];
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
export function verifyCodexTranscript(firstRecord, sessionId) {
|
|
21
|
+
return firstRecord?.type === 'session_meta' && firstRecord.payload?.id === sessionId;
|
|
22
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { withTranscriptGuard } from '../../conversations/transcript-guard.js';
|
|
3
|
+
import { compactTaskContext } from '../../conversations/task-context-view.js';
|
|
4
|
+
import { syncConversationTranscript, withTranscriptNotice, claimTranscriptBoundary, recordTranscriptTaskEntry } from '../../conversations/sync-transcript.js';
|
|
5
|
+
import { normalizeCodexRecord, verifyCodexTranscript } from './conversation-transcript.js';
|
|
6
|
+
import { hookAdditionalContextToolResult } from '../../mcp/tool-result.js';
|
|
2
7
|
import { resolve } from 'node:path';
|
|
3
8
|
import { fileURLToPath } from 'node:url';
|
|
4
9
|
|
|
@@ -37,13 +42,26 @@ export async function codexStopLifecycleOutput(input, invoke) {
|
|
|
37
42
|
return { decision: 'block', reason: boundedHookMessage(check.reason) };
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
export async function codexLifecycleOutput(event, input, invoke) {
|
|
46
|
+
if (event === 'Stop') return codexStopLifecycleOutput(input, invoke);
|
|
47
|
+
if (!input?.session_id || typeof input.cwd !== 'string' || typeof input.prompt !== 'string') {
|
|
48
|
+
throw new TypeError('Codex task entry requires session, cwd and prompt');
|
|
49
|
+
}
|
|
50
|
+
const context = await invoke('begin_task_context', { sessionId: input.session_id,
|
|
51
|
+
turnId: input.turn_id ?? null, projectPath: input.cwd, taskPrompt: input.prompt.slice(0, 8192),
|
|
52
|
+
sourceApplication: 'codex', sourceSessionId: input.session_id });
|
|
53
|
+
return JSON.parse(hookAdditionalContextToolResult(compactTaskContext(context), { hookEventName: event,
|
|
54
|
+
label: 'Fuli task context. Apply effective_preferences and checkpoint with taskContextToken.',
|
|
55
|
+
limitBytes: 128 * 1024, itemLimit: 1000 }).content[0].text);
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
export async function runCodexLifecycleHook(args, dependencies = {}) {
|
|
41
59
|
const event = args[args.indexOf('--event') + 1];
|
|
42
|
-
if (
|
|
60
|
+
if (!['Stop', 'UserPromptSubmit'].includes(event)) throw new TypeError('Missing Codex lifecycle event');
|
|
43
61
|
const input = await (dependencies.readInput ?? readHookInput)();
|
|
44
62
|
const write = dependencies.write ?? ((value) => process.stdout.write(value));
|
|
45
63
|
if (dependencies.invoke) {
|
|
46
|
-
const output = await
|
|
64
|
+
const output = await codexLifecycleOutput(event, input, dependencies.invoke);
|
|
47
65
|
write(`${JSON.stringify(output)}\n`);
|
|
48
66
|
return output;
|
|
49
67
|
}
|
|
@@ -63,12 +81,33 @@ export async function runCodexLifecycleHook(args, dependencies = {}) {
|
|
|
63
81
|
runtimeConfigPath
|
|
64
82
|
});
|
|
65
83
|
const invokeTool = dependencies.callTool ?? callAgentTool;
|
|
66
|
-
const output = await leases.withGraphLease('codex-lifecycle', () =>
|
|
67
|
-
|
|
68
|
-
input,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
const output = await leases.withGraphLease('codex-lifecycle', async () => {
|
|
85
|
+
try {
|
|
86
|
+
return await withTranscriptGuard(runtimeConfigPath, input, 'codex', app.getCapturePolicy?.().enabled, async guard => {
|
|
87
|
+
let sync = await syncConversationTranscript(app, input, 'codex', {
|
|
88
|
+
normalize: normalizeCodexRecord, verify: verifyCodexTranscript
|
|
89
|
+
}, guard);
|
|
90
|
+
if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
|
|
91
|
+
const entryGuard = sync.status === 'capture_disabled' ? null : guard;
|
|
92
|
+
if (event === 'UserPromptSubmit' && entryGuard?.value) await entryGuard.write({ ...entryGuard.value, phase: 'beginning' });
|
|
93
|
+
const result = await codexLifecycleOutput(event, input, async (name, parameters) => {
|
|
94
|
+
const context = await invokeTool(app, name, parameters);
|
|
95
|
+
if (name === 'begin_task_context') await recordTranscriptTaskEntry(entryGuard, context);
|
|
96
|
+
return context;
|
|
97
|
+
});
|
|
98
|
+
if (event === 'UserPromptSubmit') sync = await claimTranscriptBoundary(app, input, 'codex', sync, {
|
|
99
|
+
normalize: normalizeCodexRecord, verify: verifyCodexTranscript
|
|
100
|
+
}, entryGuard);
|
|
101
|
+
if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
|
|
102
|
+
return withTranscriptNotice(result, sync);
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (event !== 'UserPromptSubmit' || !app.getCapturePolicy?.().enabled || !input.transcript_path) throw error;
|
|
106
|
+
return { decision: 'block', reason: error.code === 'EADDRINUSE'
|
|
107
|
+
? 'The session handoff lock is occupied. Retry after the current hook finishes; if it persists, start a new host session and resume the intended Agent.'
|
|
108
|
+
: 'Fuli transcript handoff could not be completed safely. Retry task entry; no previous Agent context was supplied.' };
|
|
109
|
+
}
|
|
110
|
+
});
|
|
72
111
|
write(`${JSON.stringify(output)}\n`);
|
|
73
112
|
return output;
|
|
74
113
|
} finally {
|
|
@@ -18,8 +18,8 @@ export function withCodexLifecycleHooks(current, { timeout = 30, context = null
|
|
|
18
18
|
}],
|
|
19
19
|
['Stop', 'verify_task_checkpoint', { sessionId: '${session_id}' }]
|
|
20
20
|
]) {
|
|
21
|
-
const handler =
|
|
22
|
-
? { type: 'command', command: codexStopCommand(context), timeout }
|
|
21
|
+
const handler = context
|
|
22
|
+
? { type: 'command', command: codexStopCommand(context, event), timeout }
|
|
23
23
|
: { type: 'mcp_tool', server: 'fuli', tool, input, timeout };
|
|
24
24
|
hooks[event] = [...(hooks[event] ?? []), { hooks: [handler] }];
|
|
25
25
|
}
|
|
@@ -88,12 +88,12 @@ export function withCodexTomlLifecycleHooks(source, { timeout = 30, context = nu
|
|
|
88
88
|
const lines = clean.replaceAll('\r\n', '\n').split('\n');
|
|
89
89
|
while (lines.length && !lines.at(-1).trim()) lines.pop();
|
|
90
90
|
const blocks = [
|
|
91
|
-
...codexTomlHookBlock('UserPromptSubmit', 'begin_task_context', {
|
|
91
|
+
...(context ? codexTomlCommandHookBlock('UserPromptSubmit', codexStopCommand(context, 'UserPromptSubmit'), timeout) : codexTomlHookBlock('UserPromptSubmit', 'begin_task_context', {
|
|
92
92
|
sessionId: '${session_id}',
|
|
93
93
|
turnId: '${turn_id}',
|
|
94
94
|
projectPath: '${cwd}',
|
|
95
95
|
taskPrompt: '${prompt}'
|
|
96
|
-
}, timeout),
|
|
96
|
+
}, timeout)),
|
|
97
97
|
...(context
|
|
98
98
|
? codexTomlCommandHookBlock('Stop', codexStopCommand(context), timeout)
|
|
99
99
|
: codexTomlHookBlock('Stop', 'verify_task_checkpoint', {
|
|
@@ -212,7 +212,7 @@ function isManagedCodexHook(hook) {
|
|
|
212
212
|
hook.command.includes(COMMAND_MARKER) && hook.command.includes('lifecycle-hook.js');
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
function codexStopCommand(context) {
|
|
215
|
+
function codexStopCommand(context, event = 'Stop') {
|
|
216
216
|
const script = context.codexLifecycleHookPath ??
|
|
217
217
|
join(dirname(context.mcpServerPath), 'agents', 'codex', 'lifecycle-hook.js');
|
|
218
218
|
return [
|
|
@@ -222,7 +222,7 @@ function codexStopCommand(context) {
|
|
|
222
222
|
'--runtime-config',
|
|
223
223
|
context.runtimeConfigPath,
|
|
224
224
|
'--event',
|
|
225
|
-
|
|
225
|
+
event
|
|
226
226
|
].map(value => quoteShellArgument(value, context.platform)).join(' ');
|
|
227
227
|
}
|
|
228
228
|
|