fuli-context 0.7.10-beta.7 → 0.8.0
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/dist/web/assets/{AboutPage-EtWPOSpz.js → AboutPage-BjMmpwJB.js} +1 -1
- package/dist/web/assets/AgentHand-BfLYPGjg.js +1 -0
- package/dist/web/assets/AgentHand-D9sQu2vc.css +1 -0
- package/dist/web/assets/{ConnectionsPage-DXkvMuyK.js → ConnectionsPage-BKGxFDos.js} +1 -1
- package/dist/web/assets/{EmployeeRecruitDialog-DONxXnd2.js → EmployeeRecruitDialog-D-W043v4.js} +1 -1
- package/dist/web/assets/{EmployeeWorkbenchPage-Cd4hd4bf.js → EmployeeWorkbenchPage-HHQPxdqI.js} +3 -3
- package/dist/web/assets/{GrowthLoading-CCYPgE2f.js → GrowthLoading-WXCj8b0r.js} +1 -1
- package/dist/web/assets/{KnowledgeEditDialog-BUqMfa0a.js → KnowledgeEditDialog-Bkie08ZA.js} +1 -1
- package/dist/web/assets/{KnowledgeInspector-DlVPDGP3.js → KnowledgeInspector-Cez-Z_95.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-DhthZGKZ.js → KnowledgeOrganizerPage-BYmTPA_8.js} +1 -1
- package/dist/web/assets/KnowledgePage-Ck7tIBDn.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-mVFHQcxr.js → KnowledgeWorkspace-DKij2_B4.js} +1 -1
- package/dist/web/assets/{NotFoundPage-CCeEdBy9.js → NotFoundPage-HGV-q-bh.js} +1 -1
- package/dist/web/assets/{OverviewPage-CnYWoZ3r.js → OverviewPage-DE29_F_Y.js} +1 -1
- package/dist/web/assets/PersonalProfilePage-B95DFc7x.js +1 -0
- package/dist/web/assets/{PersonalProfilePage-yQRCBude.css → PersonalProfilePage-L8T5xMEC.css} +1 -1
- package/dist/web/assets/PersonalProjectsPage-Bt7tFF5Z.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-BioDR9OW.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-CiiPNe_G.js +12 -0
- package/dist/web/assets/{ProjectScopePicker-BcPtIcv8.js → ProjectScopePicker-Cu5PdxF6.js} +1 -1
- package/dist/web/assets/{PublicProjectsPage-BmBHfnud.js → PublicProjectsPage-BY4B3_t3.js} +1 -1
- package/dist/web/assets/{ReviewPage-tvt2eqhw.js → ReviewPage-C9vCesNu.js} +1 -1
- package/dist/web/assets/{SearchableSelect-BjBDBVuZ.js → SearchableSelect-214TvfYg.js} +1 -1
- package/dist/web/assets/{SettingsPage-D9mNzBg-.js → SettingsPage-D9TnzcFc.js} +1 -1
- package/dist/web/assets/{SharedJefaReportPage-Dn_HBgaa.js → SharedJefaReportPage-CdSGI_OE.js} +1 -1
- package/dist/web/assets/WritingTastePage-Bzg6AvnB.css +1 -0
- package/dist/web/assets/WritingTastePage-CUE1dU8f.js +1 -0
- package/dist/web/assets/{client-v8XKgQji.js → client-Bl4oiW3E.js} +1 -1
- package/dist/web/assets/{console-C9HS-zbd.js → console-BAIVNC-N.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DMDCpf_N.js → conversation-launcher-settings-DpRI_Jp2.js} +1 -1
- package/dist/web/assets/{i18n-BkJjUsz_.js → i18n-CVFADb_M.js} +2 -2
- package/dist/web/assets/{identity-74yGiMzO.js → identity-Dt0WZgqo.js} +1 -1
- package/dist/web/assets/index-BUMq5zNh.js +2 -0
- package/dist/web/assets/index-C0ci8dnU.css +1 -0
- package/dist/web/assets/{model-86tZgY0r.js → model-B8sOxih0.js} +1 -1
- package/dist/web/assets/personal-profile-graph-aAqex7ty.js +1 -0
- package/dist/web/assets/{shared-report-CLg82y4D.js → shared-report-BQD6l_cS.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BBUO_YEu.js → useApi-CROJJdhE-CuJ16v2S.js} +1 -1
- package/dist/web/assets/{useModalDialog-DrLhqtjR.js → useModalDialog-Ddv2XJQi.js} +1 -1
- package/dist/web/assets/{vue-router-Cw4QZxM1.js → vue-router-C8KuEZ6a.js} +1 -1
- package/dist/web/index.html +11 -11
- package/docs/project-agent-memory.md +69 -449
- package/graph-provider/fuli_graph/app.py +2 -0
- package/graph-provider/fuli_graph/preference_conflicts.py +4 -1
- package/graph-provider/fuli_graph/project_agent_coordination_models.py +23 -1
- package/graph-provider/fuli_graph/project_agent_memory_models.py +11 -0
- package/graph-provider/fuli_graph/project_agent_team_growth.py +56 -0
- package/graph-provider/fuli_graph/project_agent_work_log.py +34 -0
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +56 -2
- package/graph-provider/fuli_graph/store_project_agent_memory.py +3 -0
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +1 -1
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +2 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +17 -2
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1 -0
- package/graph-provider/fuli_graph/store_task_contexts.py +11 -1
- package/graph-provider/fuli_graph/task_context_adoption.py +65 -0
- package/graph-provider/fuli_graph/task_context_models.py +23 -1
- package/graph-provider/fuli_graph/task_context_routes.py +5 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/agent-tools/graph-definitions.js +14 -5
- package/src/agent-tools/project-agent-definitions.js +1 -0
- package/src/agents/codex/lifecycle-hook.js +1 -0
- package/src/cli/setup-command.js +1 -1
- package/src/graphiti/agent-knowledge-workflows.js +13 -4
- package/src/graphiti/project-agent-control-plane.js +9 -0
- package/src/graphiti/project-agent-mapping.js +12 -1
- package/src/graphiti/project-agent-memory.js +10 -2
- package/src/graphiti/project-agent-task-entry.js +4 -1
- package/src/graphiti/project-agent-team-context.js +21 -0
- package/src/graphiti/project-agent-workflows.js +18 -0
- package/src/graphiti/provider-client.js +7 -1
- package/src/graphiti/writing-taste-profile-workflow.js +62 -9
- package/src/graphiti/writing-taste-profile.js +43 -16
- package/src/http/graph-api-router.js +9 -0
- package/src/mcp/create-mcp-server.js +6 -4
- package/src/mcp/instructions.js +3 -3
- package/src/mcp/provider-task-context-registry.js +11 -1
- package/src/mcp/task-context-registry.js +2 -1
- package/src/mcp/tool-result.js +7 -3
- package/dist/web/assets/AgentHand-D5pOgVjJ.js +0 -1
- package/dist/web/assets/AgentHand-DvdNtJ3_.css +0 -1
- package/dist/web/assets/KnowledgePage-B6RDVxq-.js +0 -1
- package/dist/web/assets/PersonalProfilePage-Bz4zpTPC.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-2u1o6dvA.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-8tci631q.js +0 -11
- package/dist/web/assets/ProjectAgentsPage-BN4k3ubu.css +0 -1
- package/dist/web/assets/WritingTastePage-BCwimXC-.js +0 -1
- package/dist/web/assets/WritingTastePage-tb_pgude.css +0 -1
- package/dist/web/assets/index-CwjxeHGk.css +0 -1
- package/dist/web/assets/index-Dioqmmvf.js +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
|
|
3
|
-
from pydantic import Field
|
|
3
|
+
from pydantic import Field, field_validator, model_validator
|
|
4
4
|
|
|
5
5
|
from .models import StrictModel
|
|
6
6
|
|
|
@@ -12,6 +12,9 @@ class ProjectAgentCoordinationPolicyRecord(StrictModel):
|
|
|
12
12
|
personal_project_id: str
|
|
13
13
|
ask_before_recruitment: bool = True
|
|
14
14
|
auto_reuse_previous_agent: bool = True
|
|
15
|
+
auto_grow_team: bool = True
|
|
16
|
+
team_lead_agent_id: str | None = None
|
|
17
|
+
team_member_agent_ids: list[str] = Field(default_factory=list)
|
|
15
18
|
updated_at: datetime | None = None
|
|
16
19
|
|
|
17
20
|
|
|
@@ -20,3 +23,22 @@ class ProjectAgentCoordinationPolicyUpdate(StrictModel):
|
|
|
20
23
|
personal_project_id: str = Field(min_length=1, max_length=128)
|
|
21
24
|
ask_before_recruitment: bool = True
|
|
22
25
|
auto_reuse_previous_agent: bool = True
|
|
26
|
+
auto_grow_team: bool | None = None
|
|
27
|
+
team_lead_agent_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
28
|
+
team_member_agent_ids: list[str] = Field(default_factory=list, max_length=32)
|
|
29
|
+
expected_updated_at: datetime | None = None
|
|
30
|
+
|
|
31
|
+
@field_validator('team_member_agent_ids')
|
|
32
|
+
@classmethod
|
|
33
|
+
def validate_members(cls, value):
|
|
34
|
+
if any(not item.strip() or len(item) > 128 for item in value):
|
|
35
|
+
raise ValueError('team member IDs must contain 1 to 128 characters')
|
|
36
|
+
if len(set(value)) != len(value):
|
|
37
|
+
raise ValueError('team member IDs must be unique')
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
@model_validator(mode='after')
|
|
41
|
+
def validate_team(self):
|
|
42
|
+
if self.team_lead_agent_id in self.team_member_agent_ids:
|
|
43
|
+
raise ValueError('the team lead cannot also be a team member')
|
|
44
|
+
return self
|
|
@@ -65,6 +65,16 @@ class ProjectAgentMemoryRecord(StrictModel):
|
|
|
65
65
|
created_at: datetime
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
class EmployeeWorkLogRecord(StrictModel):
|
|
69
|
+
task_context_token: str
|
|
70
|
+
source_application: SourceApplication
|
|
71
|
+
session_id: str
|
|
72
|
+
created_at: datetime
|
|
73
|
+
status: Literal['reported', 'completed', 'incomplete', 'failed', 'no_change', 'unreported', 'running']
|
|
74
|
+
summary: str = Field(min_length=1, max_length=2000)
|
|
75
|
+
memory_updated: bool
|
|
76
|
+
|
|
77
|
+
|
|
68
78
|
class ProjectAgentMemoryView(StrictModel):
|
|
69
79
|
personal_space_id: str
|
|
70
80
|
personal_project_id: str
|
|
@@ -77,3 +87,4 @@ class ProjectAgentMemoryView(StrictModel):
|
|
|
77
87
|
revision: int = Field(default=0, ge=0)
|
|
78
88
|
current: ProjectAgentMemoryRecord | None = None
|
|
79
89
|
history: list[ProjectAgentMemoryRecord] = Field(default_factory=list)
|
|
90
|
+
work_log: list[EmployeeWorkLogRecord] = Field(default_factory=list, max_length=6)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Grow a project team inside the recruitment transaction, without changing authority."""
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
|
|
5
|
+
from .provider_values import now_utc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def enroll_recruited_team_member(store, recruitment, profile):
|
|
9
|
+
# Temporary workers and system employees do not become permanent teammates.
|
|
10
|
+
if (profile.agent_type != 'durable' or profile.status != 'active'
|
|
11
|
+
or any(cap.startswith('fuli.employee:') for cap in profile.capabilities)):
|
|
12
|
+
return
|
|
13
|
+
space_id = recruitment['personal_space_id']
|
|
14
|
+
project_id = recruitment['personal_project_id']
|
|
15
|
+
rows, _, _ = await store.runtime.driver.execute_query(
|
|
16
|
+
'''
|
|
17
|
+
MATCH (:FuliSpace {id: $space_id})-[:CONTAINS_PROJECT]->
|
|
18
|
+
(project:FuliPersonalProject {project_id: $project_id})
|
|
19
|
+
MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
20
|
+
(:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->
|
|
21
|
+
(agent:FuliProjectAgent {agent_id: $agent_id, status: 'active'})
|
|
22
|
+
WITH DISTINCT project, agent
|
|
23
|
+
MERGE (policy:FuliProjectAgentCoordinationPolicy {policy_id: $policy_id})
|
|
24
|
+
ON CREATE SET policy.personal_space_id = $space_id,
|
|
25
|
+
policy.personal_project_id = $project_id,
|
|
26
|
+
policy.created_at = $updated_at
|
|
27
|
+
SET policy._fuli_write_lock = true
|
|
28
|
+
REMOVE policy._fuli_write_lock
|
|
29
|
+
MERGE (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy)
|
|
30
|
+
WITH policy, agent, coalesce(policy.auto_grow_team, true) AS enabled,
|
|
31
|
+
coalesce(policy.team_member_agent_ids, []) AS members
|
|
32
|
+
WITH policy, agent, members, enabled,
|
|
33
|
+
policy.team_lead_agent_id IS NULL AS needs_lead,
|
|
34
|
+
coalesce(policy.team_lead_agent_id = agent.agent_id, false)
|
|
35
|
+
OR agent.agent_id IN members AS already_member
|
|
36
|
+
WITH *, enabled AND NOT already_member
|
|
37
|
+
AND (needs_lead OR size(members) < 32) AS changed
|
|
38
|
+
FOREACH (_ IN CASE WHEN changed THEN [1] ELSE [] END |
|
|
39
|
+
SET policy.team_lead_agent_id = CASE WHEN needs_lead
|
|
40
|
+
THEN agent.agent_id ELSE policy.team_lead_agent_id END,
|
|
41
|
+
policy.team_member_agent_ids = CASE WHEN needs_lead
|
|
42
|
+
THEN members ELSE members + agent.agent_id END,
|
|
43
|
+
policy.updated_at = $updated_at
|
|
44
|
+
)
|
|
45
|
+
RETURN enabled AND NOT already_member AND NOT needs_lead
|
|
46
|
+
AND size(members) >= 32 AS full
|
|
47
|
+
''',
|
|
48
|
+
space_id=space_id, project_id=project_id,
|
|
49
|
+
agent_id=recruitment['proposed_agent_id'],
|
|
50
|
+
policy_id=store._project_agent_coordination_policy_id(space_id, project_id),
|
|
51
|
+
updated_at=now_utc(),
|
|
52
|
+
)
|
|
53
|
+
if not rows:
|
|
54
|
+
raise HTTPException(409, 'recruited Agent has no active project assignment')
|
|
55
|
+
if rows[0]['full']:
|
|
56
|
+
raise HTTPException(409, 'project team is full; change the team before recruiting')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Recover bounded employee work records independently of distilled memory updates."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def read_employee_work_log(store, space_id, project_id, agent_id):
|
|
7
|
+
rows, _, _ = await store.runtime.driver.execute_query(
|
|
8
|
+
'''
|
|
9
|
+
MATCH (:FuliSpace {id: $space_id})-[:HAS_TASK_CONTEXT_SESSION]->
|
|
10
|
+
(session:FuliTaskContextSession)-[:HAS_CONTEXT]->(task:FuliTaskContext)
|
|
11
|
+
WHERE task.personal_project_id = $project_id AND task.project_agent_id = $agent_id
|
|
12
|
+
RETURN task.record_json AS record_json, task.completed AS completed,
|
|
13
|
+
task.agent_memory_claimed AS memory_updated,
|
|
14
|
+
task.token = session.current_token AS current
|
|
15
|
+
ORDER BY task.created_at DESC, task.token LIMIT 6
|
|
16
|
+
''', space_id=space_id, project_id=project_id, agent_id=agent_id, routing_='r',
|
|
17
|
+
)
|
|
18
|
+
result = []
|
|
19
|
+
for row in rows:
|
|
20
|
+
record = json.loads(row['record_json'])
|
|
21
|
+
checkpoint = record.get('checkpoint') or {}
|
|
22
|
+
log = checkpoint.get('work_log') or {}
|
|
23
|
+
result.append({
|
|
24
|
+
'task_context_token': record['token'],
|
|
25
|
+
'source_application': record['source_application'],
|
|
26
|
+
'session_id': record['session_id'],
|
|
27
|
+
'created_at': record['created_at'],
|
|
28
|
+
'status': (log.get('status', 'unreported') if row['completed']
|
|
29
|
+
else 'running' if row.get('current') else 'incomplete'),
|
|
30
|
+
'summary': log.get('summary') or checkpoint.get('reason')
|
|
31
|
+
or 'The employee has not submitted a work summary for this task.',
|
|
32
|
+
'memory_updated': bool(row.get('memory_updated')),
|
|
33
|
+
})
|
|
34
|
+
return result
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Project-local Agent continuity and recruitment authorization."""
|
|
2
2
|
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
|
|
3
5
|
from .personal_project_access import authorize_personal_project
|
|
4
6
|
from .project_agent_coordination_models import (
|
|
5
7
|
ProjectAgentCoordinationPolicyRecord,
|
|
@@ -9,7 +11,7 @@ from .provider_values import native_datetime, now_utc, stable_uuid
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class StoreProjectAgentCoordinationPolicy:
|
|
12
|
-
"""Persist
|
|
14
|
+
"""Persist project automation and a stable team without absorbing peer roles."""
|
|
13
15
|
|
|
14
16
|
async def get_project_agent_coordination_policy(
|
|
15
17
|
self,
|
|
@@ -53,6 +55,9 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
53
55
|
'auto_reuse_previous_agent',
|
|
54
56
|
True,
|
|
55
57
|
),
|
|
58
|
+
team_lead_agent_id=raw.get('team_lead_agent_id'),
|
|
59
|
+
auto_grow_team=raw.get('auto_grow_team', True),
|
|
60
|
+
team_member_agent_ids=raw.get('team_member_agent_ids') or [],
|
|
56
61
|
updated_at=native_datetime(raw.get('updated_at')),
|
|
57
62
|
)
|
|
58
63
|
|
|
@@ -77,8 +82,38 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
77
82
|
request.personal_space_id,
|
|
78
83
|
request.personal_project_id,
|
|
79
84
|
)
|
|
85
|
+
current = await self.get_project_agent_coordination_policy(
|
|
86
|
+
actor, request.personal_space_id, request.personal_project_id,
|
|
87
|
+
)
|
|
88
|
+
if ('expected_updated_at' in request.model_fields_set
|
|
89
|
+
and request.expected_updated_at != current.updated_at):
|
|
90
|
+
raise HTTPException(status_code=409, detail='coordination policy changed; reload before saving')
|
|
91
|
+
lead = (request.team_lead_agent_id if 'team_lead_agent_id' in request.model_fields_set
|
|
92
|
+
else current.team_lead_agent_id)
|
|
93
|
+
members = (request.team_member_agent_ids if 'team_member_agent_ids' in request.model_fields_set
|
|
94
|
+
else current.team_member_agent_ids)
|
|
95
|
+
auto_grow = current.auto_grow_team if request.auto_grow_team is None else request.auto_grow_team
|
|
96
|
+
if members and not lead:
|
|
97
|
+
raise HTTPException(status_code=422, detail='select a team lead before adding members')
|
|
98
|
+
if lead in members:
|
|
99
|
+
raise HTTPException(status_code=422, detail='the team lead cannot also be a team member')
|
|
100
|
+
if 'team_lead_agent_id' in request.model_fields_set or 'team_member_agent_ids' in request.model_fields_set:
|
|
101
|
+
for agent_id in ([lead] if lead else []) + members:
|
|
102
|
+
agent = await self.get_project_agent(
|
|
103
|
+
actor, request.personal_space_id, request.personal_project_id, agent_id,
|
|
104
|
+
)
|
|
105
|
+
profile = agent.profile
|
|
106
|
+
active_assignment = any(
|
|
107
|
+
assignment.status == 'active'
|
|
108
|
+
and assignment.personal_project_id == request.personal_project_id
|
|
109
|
+
for assignment in agent.assignments
|
|
110
|
+
)
|
|
111
|
+
if (profile.status != 'active' or not active_assignment
|
|
112
|
+
or profile.agent_type != 'durable'
|
|
113
|
+
or any(cap.startswith('fuli.employee:') for cap in profile.capabilities)):
|
|
114
|
+
raise HTTPException(status_code=422, detail='team roles require active durable project assignments; HR and project managers remain peers')
|
|
80
115
|
updated_at = now_utc()
|
|
81
|
-
await self.runtime.driver.execute_query(
|
|
116
|
+
written, _, _ = await self.runtime.driver.execute_query(
|
|
82
117
|
'''
|
|
83
118
|
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
84
119
|
[:CONTAINS_PROJECT]->
|
|
@@ -89,23 +124,42 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
89
124
|
ON CREATE SET policy.personal_space_id = $personal_space_id,
|
|
90
125
|
policy.personal_project_id = $personal_project_id,
|
|
91
126
|
policy.created_at = $updated_at
|
|
127
|
+
SET policy._fuli_write_lock = true
|
|
128
|
+
REMOVE policy._fuli_write_lock
|
|
129
|
+
WITH project, policy
|
|
130
|
+
WHERE ($expected_updated_at IS NULL AND policy.updated_at IS NULL)
|
|
131
|
+
OR (policy.updated_at.epochSeconds = $expected_updated_at.epochSeconds
|
|
132
|
+
AND policy.updated_at.nanosecond = $expected_updated_at.nanosecond)
|
|
92
133
|
SET policy.ask_before_recruitment = $ask_before_recruitment,
|
|
93
134
|
policy.auto_reuse_previous_agent = $auto_reuse_previous_agent,
|
|
135
|
+
policy.auto_grow_team = $auto_grow_team,
|
|
136
|
+
policy.team_lead_agent_id = $team_lead_agent_id,
|
|
137
|
+
policy.team_member_agent_ids = $team_member_agent_ids,
|
|
94
138
|
policy.updated_at = $updated_at
|
|
95
139
|
MERGE (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy)
|
|
140
|
+
RETURN policy
|
|
96
141
|
''',
|
|
97
142
|
policy_id=policy_id,
|
|
98
143
|
personal_space_id=request.personal_space_id,
|
|
99
144
|
personal_project_id=request.personal_project_id,
|
|
100
145
|
ask_before_recruitment=request.ask_before_recruitment,
|
|
101
146
|
auto_reuse_previous_agent=request.auto_reuse_previous_agent,
|
|
147
|
+
auto_grow_team=auto_grow,
|
|
148
|
+
team_lead_agent_id=lead,
|
|
149
|
+
team_member_agent_ids=members,
|
|
150
|
+
expected_updated_at=current.updated_at,
|
|
102
151
|
updated_at=updated_at,
|
|
103
152
|
)
|
|
153
|
+
if not written:
|
|
154
|
+
raise HTTPException(status_code=409, detail='coordination policy changed; reload before saving')
|
|
104
155
|
return ProjectAgentCoordinationPolicyRecord(
|
|
105
156
|
personal_space_id=request.personal_space_id,
|
|
106
157
|
personal_project_id=request.personal_project_id,
|
|
107
158
|
ask_before_recruitment=request.ask_before_recruitment,
|
|
108
159
|
auto_reuse_previous_agent=request.auto_reuse_previous_agent,
|
|
160
|
+
auto_grow_team=auto_grow,
|
|
161
|
+
team_lead_agent_id=lead,
|
|
162
|
+
team_member_agent_ids=members,
|
|
109
163
|
updated_at=updated_at,
|
|
110
164
|
)
|
|
111
165
|
|
|
@@ -16,6 +16,7 @@ from .project_agent_memory_models import (
|
|
|
16
16
|
ProjectAgentMemoryWrite,
|
|
17
17
|
)
|
|
18
18
|
from .provider_values import now_utc, stable_uuid
|
|
19
|
+
from .project_agent_work_log import read_employee_work_log
|
|
19
20
|
from .system_hr_identity import LEGACY_HR_AGENT_ID, SYSTEM_HR_AGENT_ID, resolve_hr_alias
|
|
20
21
|
|
|
21
22
|
|
|
@@ -53,6 +54,8 @@ class StoreProjectAgentMemory:
|
|
|
53
54
|
revision=current.revision if current else 0,
|
|
54
55
|
current=current,
|
|
55
56
|
history=history,
|
|
57
|
+
work_log=await read_employee_work_log(self, personal_space_id,
|
|
58
|
+
personal_project_id, agent['agent_id']),
|
|
56
59
|
)
|
|
57
60
|
|
|
58
61
|
async def write_project_agent_memory(
|
|
@@ -266,7 +266,7 @@ class StoreProjectAgentTaskActivity:
|
|
|
266
266
|
THEN [1] ELSE [] END |
|
|
267
267
|
SET task.status = $status,
|
|
268
268
|
task.result_summary = CASE
|
|
269
|
-
WHEN $status
|
|
269
|
+
WHEN $status IN ['completed', 'awaiting_review', 'failed', 'blocked', 'cancelled'] THEN $summary
|
|
270
270
|
ELSE task.result_summary END,
|
|
271
271
|
task.failure_reason = CASE
|
|
272
272
|
WHEN $status = 'failed' THEN $summary
|
|
@@ -26,6 +26,7 @@ from .project_agent_task_models import (
|
|
|
26
26
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
27
27
|
from .store_project_agents import SYSTEM_HR_AGENT_ID
|
|
28
28
|
from .store_transactions import query_store_transaction
|
|
29
|
+
from .project_agent_team_growth import enroll_recruited_team_member
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
RECRUITMENT_CLAIM_TTL = timedelta(minutes=2)
|
|
@@ -1236,6 +1237,7 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
1236
1237
|
include_temporary=True,
|
|
1237
1238
|
)
|
|
1238
1239
|
selected = next(item for item in assignment_rows if item['agent_id'] == agent_id)
|
|
1240
|
+
await enroll_recruited_team_member(self, raw, profile)
|
|
1239
1241
|
return selected
|
|
1240
1242
|
|
|
1241
1243
|
async def _get_recruitment(self, personal_space_id, recruitment_id):
|
|
@@ -62,6 +62,18 @@ class StoreProjectAgentTaskStaffing:
|
|
|
62
62
|
return [], rows, 'manual_agent_selection', [
|
|
63
63
|
'project policy requires an explicit @Agent selection',
|
|
64
64
|
]
|
|
65
|
+
team_lead = getattr(policy, 'team_lead_agent_id', None)
|
|
66
|
+
if team_lead:
|
|
67
|
+
lead = next((row for row in rows if row['agent_id'] == team_lead), None)
|
|
68
|
+
if lead is None:
|
|
69
|
+
return [], rows, 'agent_unavailable', ['configured team lead is unavailable; select or replace the lead']
|
|
70
|
+
member_ids = set(getattr(policy, 'team_member_agent_ids', []))
|
|
71
|
+
members = [row for row in rows if row['agent_id'] in member_ids]
|
|
72
|
+
return [lead], [lead, *members], 'project_team_lead', [
|
|
73
|
+
'project team lead is the stable conversation owner',
|
|
74
|
+
f'{len(members)} active team member(s) available for explicit workstreams',
|
|
75
|
+
'HR and project manager remain peer roles',
|
|
76
|
+
]
|
|
65
77
|
normalized_work_kind = request.work_kind.casefold()
|
|
66
78
|
required = {item.casefold() for item in request.required_capabilities}
|
|
67
79
|
eligible = [row for row in rows if required.issubset({
|
|
@@ -506,16 +518,19 @@ class StoreProjectAgentTaskStaffing:
|
|
|
506
518
|
request,
|
|
507
519
|
candidates,
|
|
508
520
|
participants,
|
|
521
|
+
*,
|
|
522
|
+
team_only=False,
|
|
509
523
|
):
|
|
510
524
|
capacity = len(request.parallel_plan.workstream_boundaries) - len(participants)
|
|
511
525
|
if capacity <= 0:
|
|
512
526
|
return []
|
|
513
527
|
pool = list(candidates)
|
|
514
528
|
known_ids = {item['agent_id'] for item in pool}
|
|
515
|
-
|
|
529
|
+
assignments = [] if team_only else await self._assignment_candidates(
|
|
516
530
|
request.personal_space_id,
|
|
517
531
|
request.personal_project_id,
|
|
518
|
-
)
|
|
532
|
+
)
|
|
533
|
+
for row in assignments:
|
|
519
534
|
if row['agent_id'] not in known_ids:
|
|
520
535
|
pool.append(row)
|
|
521
536
|
known_ids.add(row['agent_id'])
|
|
@@ -14,9 +14,13 @@ from .project_agent_access import authorize_project_agent
|
|
|
14
14
|
from .provider_values import now_utc, stable_uuid
|
|
15
15
|
from .project_agent_memory_models import ProjectAgentMemoryWrite
|
|
16
16
|
from .store_transactions import query_store_transaction
|
|
17
|
+
from .task_context_adoption import adopt_task_agent
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class StoreTaskContexts:
|
|
21
|
+
async def adopt_task_context_agent(self, actor, token, request):
|
|
22
|
+
return await adopt_task_agent(self, actor, token, request)
|
|
23
|
+
|
|
20
24
|
async def begin_task_context(self, actor, request):
|
|
21
25
|
self._require_personal()
|
|
22
26
|
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
@@ -133,6 +137,8 @@ class StoreTaskContexts:
|
|
|
133
137
|
record = await self.get_task_context(
|
|
134
138
|
actor, request.personal_space_id, token, request.source_application,
|
|
135
139
|
)
|
|
140
|
+
if record.get('work_log_required') and request.work_log is None:
|
|
141
|
+
raise HTTPException(422, 'Employee work summary is required before checkpointing')
|
|
136
142
|
checkpoint = request.model_dump(mode='json', exclude={
|
|
137
143
|
'personal_space_id', 'source_application', 'agent_memory',
|
|
138
144
|
})
|
|
@@ -163,12 +169,14 @@ class StoreTaskContexts:
|
|
|
163
169
|
WITH task, first_claim OR (
|
|
164
170
|
task.fingerprint = $fingerprint AND task.disposition = $disposition
|
|
165
171
|
AND task.reason = $reason
|
|
172
|
+
AND coalesce(task.work_log_json, 'null') = $work_log_json
|
|
166
173
|
AND (NOT $is_prepare OR memory_claimed = $has_agent_memory)
|
|
167
174
|
) AS matches,
|
|
168
175
|
CASE WHEN first_claim THEN $has_agent_memory ELSE memory_claimed END AS memory_claimed
|
|
169
176
|
FOREACH (ignored IN CASE WHEN matches AND NOT task.completed THEN [1] ELSE [] END |
|
|
170
177
|
SET task.fingerprint = $fingerprint, task.record_json = $record_json,
|
|
171
178
|
task.agent_memory_claimed = memory_claimed,
|
|
179
|
+
task.work_log_json = $work_log_json,
|
|
172
180
|
task.completed = $completed, task.disposition = $disposition, task.reason = $reason
|
|
173
181
|
)
|
|
174
182
|
RETURN matches, task.record_json AS record_json
|
|
@@ -178,6 +186,8 @@ class StoreTaskContexts:
|
|
|
178
186
|
completed=request.phase == 'complete',
|
|
179
187
|
is_prepare=request.phase == 'prepare',
|
|
180
188
|
has_agent_memory=request.agent_memory is not None,
|
|
189
|
+
work_log_json=json.dumps(request.work_log.model_dump() if request.work_log else None,
|
|
190
|
+
sort_keys=True, ensure_ascii=False),
|
|
181
191
|
memory_checkpoint_id=memory_checkpoint_id,
|
|
182
192
|
)
|
|
183
193
|
if not rows or not rows[0]['matches']:
|
|
@@ -208,7 +218,7 @@ class StoreTaskContexts:
|
|
|
208
218
|
'task_context_token': record['token'],
|
|
209
219
|
'reason': f'FULI_CHECKPOINT_REQUIRED: {record["token"]} '
|
|
210
220
|
'Before finishing, call checkpoint_task_knowledge using this task_context_token '
|
|
211
|
-
'with capture_candidates or retain_nothing; include agentMemory when durable '
|
|
221
|
+
'with capture_candidates or retain_nothing and workLog (summary and truthful status); include agentMemory when durable '
|
|
212
222
|
'role context changed. Never store raw transcripts or credentials.',
|
|
213
223
|
}
|
|
214
224
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Claim an initially unassigned task after recruitment, never replace an owner."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from fastapi import HTTPException
|
|
6
|
+
from .project_agent_access import authorize_project_agent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def adopt_task_agent(store, actor, token, request):
|
|
10
|
+
space = await store.authorize(actor, request.personal_space_id, 'maintainer')
|
|
11
|
+
record = await store.get_task_context(actor, request.personal_space_id, token,
|
|
12
|
+
request.source_application)
|
|
13
|
+
if record['personal_project_id'] != request.personal_project_id:
|
|
14
|
+
raise HTTPException(409, 'Task context belongs to another project')
|
|
15
|
+
if record.get('project_agent_id'):
|
|
16
|
+
if record['project_agent_id'] == request.agent_id:
|
|
17
|
+
return record
|
|
18
|
+
raise HTTPException(409, 'Task context already has an employee')
|
|
19
|
+
before = json.dumps(record, ensure_ascii=False)
|
|
20
|
+
agent = await authorize_project_agent(store, actor, space, request.personal_project_id,
|
|
21
|
+
request.agent_id, require_active=True, require_memory=True)
|
|
22
|
+
if request.source_application not in json.loads(agent['profile_json']).get('allowed_clients', []):
|
|
23
|
+
raise HTTPException(403, 'Agent is not allowed in this client')
|
|
24
|
+
memory = await store.get_project_agent_memory(actor, request.personal_space_id,
|
|
25
|
+
request.personal_project_id, request.agent_id)
|
|
26
|
+
record.update(project_agent_id=request.agent_id, memory_revision=memory.revision,
|
|
27
|
+
work_log_required=True)
|
|
28
|
+
rows, _, _ = await store.runtime.driver.execute_query(
|
|
29
|
+
'''
|
|
30
|
+
MATCH (space:FuliSpace {id: $space_id})-[:HAS_TASK_CONTEXT_SESSION]->
|
|
31
|
+
(session:FuliTaskContextSession)-[:HAS_CONTEXT]->(context:FuliTaskContext {token: $token})
|
|
32
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_TASK]->(task:FuliProjectAgentTask {
|
|
33
|
+
task_id: $task_id, personal_project_id: $project_id, lead_agent_id: $agent_id})
|
|
34
|
+
SET session.write_serial = coalesce(session.write_serial, 0) + 1
|
|
35
|
+
WITH context, session, task
|
|
36
|
+
SET task._task_lifecycle_lock = true
|
|
37
|
+
REMOVE task._task_lifecycle_lock
|
|
38
|
+
WITH context, session, task
|
|
39
|
+
MATCH (task)-[:HAS_PARTICIPANT]->(agent:FuliProjectAgent {agent_id: $agent_id, status: 'active'})
|
|
40
|
+
SET agent._task_lifecycle_lock = true
|
|
41
|
+
REMOVE agent._task_lifecycle_lock
|
|
42
|
+
WITH context, session, agent, task
|
|
43
|
+
WHERE context.token = session.current_token AND context.project_agent_id IS NULL
|
|
44
|
+
AND context.fingerprint IS NULL AND NOT context.completed
|
|
45
|
+
AND context.record_json = $before
|
|
46
|
+
AND agent.profile_json = $profile_json
|
|
47
|
+
AND task.status IN ['queued', 'running', 'paused', 'blocked', 'awaiting_recruitment']
|
|
48
|
+
AND EXISTS {
|
|
49
|
+
MATCH (:FuliSpace {id: $space_id})-[:CONTAINS_PROJECT]->
|
|
50
|
+
(:FuliPersonalProject {project_id: $project_id})-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
51
|
+
(:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->(agent)
|
|
52
|
+
}
|
|
53
|
+
SET context.project_agent_id = agent.agent_id, context.record_json = $record_json
|
|
54
|
+
RETURN context.record_json AS record_json
|
|
55
|
+
''', space_id=request.personal_space_id, project_id=request.personal_project_id,
|
|
56
|
+
task_id=request.task_id, agent_id=request.agent_id, token=token,
|
|
57
|
+
before=before, record_json=json.dumps(record, ensure_ascii=False), profile_json=agent['profile_json'],
|
|
58
|
+
)
|
|
59
|
+
if not rows:
|
|
60
|
+
latest = await store.get_task_context(actor, request.personal_space_id, token,
|
|
61
|
+
request.source_application)
|
|
62
|
+
if latest.get('project_agent_id') == request.agent_id:
|
|
63
|
+
return latest
|
|
64
|
+
raise HTTPException(409, 'Task employee claim changed, completed, or is not the routed lead')
|
|
65
|
+
return json.loads(rows[0]['record_json'])
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
|
-
from pydantic import Field, model_validator
|
|
5
|
+
from pydantic import Field, field_validator, model_validator
|
|
6
6
|
|
|
7
7
|
from .models import SourceApplication, StrictModel
|
|
8
8
|
from .model_validation import reject_credentials
|
|
@@ -19,6 +19,7 @@ class TaskContextBegin(StrictModel):
|
|
|
19
19
|
token: str = Field(pattern=r'^fuli-task-[a-zA-Z0-9-]{8,128}$')
|
|
20
20
|
turn_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
21
21
|
memory_revision: int | None = Field(default=None, ge=0)
|
|
22
|
+
work_log_required: bool = False
|
|
22
23
|
|
|
23
24
|
@model_validator(mode='after')
|
|
24
25
|
def require_project_for_agent(self):
|
|
@@ -32,6 +33,26 @@ class TaskContextAgentMemory(StrictModel):
|
|
|
32
33
|
memory: ProjectAgentWorkingMemory
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
class TaskContextAdoptAgent(StrictModel):
|
|
37
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
38
|
+
personal_project_id: str = Field(min_length=1, max_length=128)
|
|
39
|
+
source_application: SourceApplication
|
|
40
|
+
task_id: str = Field(min_length=1, max_length=128)
|
|
41
|
+
agent_id: str = Field(min_length=1, max_length=128)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TaskWorkLog(StrictModel):
|
|
45
|
+
status: Literal['reported', 'completed', 'incomplete', 'failed', 'no_change', 'unreported']
|
|
46
|
+
summary: str = Field(min_length=1, max_length=2000)
|
|
47
|
+
|
|
48
|
+
@field_validator('summary')
|
|
49
|
+
@classmethod
|
|
50
|
+
def require_summary(cls, value):
|
|
51
|
+
if not value.strip():
|
|
52
|
+
raise ValueError('Work summary must not be blank')
|
|
53
|
+
return value.strip()
|
|
54
|
+
|
|
55
|
+
|
|
35
56
|
class TaskContextCheckpoint(StrictModel):
|
|
36
57
|
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
37
58
|
source_application: SourceApplication
|
|
@@ -41,6 +62,7 @@ class TaskContextCheckpoint(StrictModel):
|
|
|
41
62
|
fingerprint: str = Field(pattern=r'^[a-f0-9]{64}$')
|
|
42
63
|
capture_status: str | None = Field(default=None, max_length=128)
|
|
43
64
|
agent_memory: TaskContextAgentMemory | None = None
|
|
65
|
+
work_log: TaskWorkLog | None = None
|
|
44
66
|
|
|
45
67
|
@model_validator(mode='after')
|
|
46
68
|
def reject_sensitive_values(self):
|
|
@@ -3,10 +3,14 @@ from typing import Annotated
|
|
|
3
3
|
from fastapi import Query
|
|
4
4
|
|
|
5
5
|
from .models import SourceApplication
|
|
6
|
-
from .task_context_models import TaskContextBegin, TaskContextCheckpoint
|
|
6
|
+
from .task_context_models import TaskContextBegin, TaskContextCheckpoint, TaskContextAdoptAgent
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def register_task_context_routes(application, store, Actor):
|
|
10
|
+
@application.put('/v1/task-contexts/{token}/agent')
|
|
11
|
+
async def adopt_agent(token: str, request: TaskContextAdoptAgent, actor: Actor):
|
|
12
|
+
return await store.adopt_task_context_agent(actor, token, request)
|
|
13
|
+
|
|
10
14
|
@application.put('/v1/task-contexts')
|
|
11
15
|
async def begin_context(request: TaskContextBegin, actor: Actor):
|
|
12
16
|
return await store.begin_task_context(actor, request)
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fuli-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "fuli-context",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.8.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@hono/node-server": "2.0.12",
|
package/package.json
CHANGED
|
@@ -98,7 +98,8 @@ const knowledgeConflictResolution = enumSchema([
|
|
|
98
98
|
]);
|
|
99
99
|
const profileAspect = {
|
|
100
100
|
type: ['string', 'null'],
|
|
101
|
-
enum: ['taste', 'personality', 'judgment_preference', null]
|
|
101
|
+
enum: ['taste', 'personality', 'judgment_preference', null],
|
|
102
|
+
description: 'Use "taste" for durable stylistic preference about the artifact itself, including writing voice, tone, wording, structure, formatting, and visual style; a writing or formatting style rule belongs here, not in judgment_preference. Use "judgment_preference" only for how a decision or process should be made, such as verification, risk, or sequencing rules. Use "personality" only for stable collaboration personality; an Agent-inferred personality item must stay confirmationStatus "pending" until a human confirms it.'
|
|
102
103
|
};
|
|
103
104
|
const sourceApplication = {
|
|
104
105
|
type: ['string', 'null'],
|
|
@@ -301,11 +302,15 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
301
302
|
{
|
|
302
303
|
name: 'checkpoint_task_knowledge',
|
|
303
304
|
title: 'LIFECYCLE · Finish knowledge review',
|
|
304
|
-
description: 'Complete exactly one end-of-task knowledge review. Use capture_candidates only for a small durable batch supported by the task; captured Agent proposals remain pending unless the payload contains valid human or authoritative-source confirmation. Use retain_nothing when the turn produced no reusable knowledge. Never store raw transcripts, guesses, temporary logs, credentials, or disposable output.',
|
|
305
|
+
description: 'Complete exactly one end-of-task knowledge review. An assigned employee must include workLog with a concise result summary and truthful status even with retain_nothing. Include agentMemory with the loaded revision and merged notes when durable role memory changed. Use capture_candidates only for a small durable batch supported by the task; captured Agent proposals remain pending unless the payload contains valid human or authoritative-source confirmation. Use retain_nothing when the turn produced no reusable knowledge. Never store raw transcripts, guesses, temporary logs, credentials, or disposable output.',
|
|
305
306
|
inputSchema: objectSchema({
|
|
306
307
|
taskContextToken: id,
|
|
307
308
|
disposition: enumSchema(['capture_candidates', 'retain_nothing']),
|
|
308
309
|
reason: boundedString(2000),
|
|
310
|
+
workLog: objectSchema({
|
|
311
|
+
status: enumSchema(['reported', 'completed', 'incomplete', 'failed', 'no_change']),
|
|
312
|
+
summary: boundedString(2000)
|
|
313
|
+
}, ['status', 'summary']),
|
|
309
314
|
capture: objectSchema(captureEpisodeFields, captureEpisodeRequired),
|
|
310
315
|
agentMemory: objectSchema({
|
|
311
316
|
expectedRevision: integerSchema({ minimum: 0 }),
|
|
@@ -743,7 +748,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
743
748
|
{
|
|
744
749
|
name: 'coordinate_project_agent_task',
|
|
745
750
|
title: 'WRITE · Prepare a host-executed Agent team',
|
|
746
|
-
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. 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.',
|
|
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.',
|
|
747
752
|
inputSchema: projectAgentTaskCoordinateInput
|
|
748
753
|
},
|
|
749
754
|
{
|
|
@@ -815,12 +820,16 @@ export const GRAPH_TOOL_DEFINITIONS = [
|
|
|
815
820
|
{
|
|
816
821
|
name: 'update_project_agent_coordination_policy',
|
|
817
822
|
title: 'WRITE · Set project Agent continuity policy',
|
|
818
|
-
description: 'Persist
|
|
823
|
+
description: 'Persist project collaboration switches and an optional stable team lead with members. HR and employee project managers remain peer roles and cannot be team members. Omitted team fields preserve the current team. Read policy first and send expectedUpdatedAt to prevent overwriting concurrent edits. This changes policy only and never starts a worker.',
|
|
819
824
|
inputSchema: objectSchema({
|
|
820
825
|
personalSpaceId: id,
|
|
821
826
|
personalProjectId: id,
|
|
822
827
|
askBeforeRecruitment: booleanSchema(),
|
|
823
|
-
autoReusePreviousAgent: booleanSchema()
|
|
828
|
+
autoReusePreviousAgent: booleanSchema(),
|
|
829
|
+
autoGrowTeam: booleanSchema(),
|
|
830
|
+
teamLeadAgentId: nullableStringSchema(),
|
|
831
|
+
teamMemberAgentIds: arraySchema(id, { maxItems: 32, uniqueItems: true }),
|
|
832
|
+
expectedUpdatedAt: { ...dateTime, type: ['string', 'null'] }
|
|
824
833
|
}, [
|
|
825
834
|
'personalSpaceId', 'personalProjectId',
|
|
826
835
|
'askBeforeRecruitment', 'autoReusePreviousAgent'
|
|
@@ -154,6 +154,7 @@ export const projectAgentTaskSubmitInput = objectSchema({
|
|
|
154
154
|
'workKind', 'routingReason'
|
|
155
155
|
]);
|
|
156
156
|
export const projectAgentTaskCoordinateInput = objectSchema({
|
|
157
|
+
taskContextToken: id,
|
|
157
158
|
projectPath: boundedString(4096),
|
|
158
159
|
idempotencyKey,
|
|
159
160
|
title: boundedString(160),
|