Skip to content

Commit fa649da

Browse files
author
SqlRush
committed
Apply coordinator team schedule plans
1 parent 2a3abd4 commit fa649da

4 files changed

Lines changed: 223 additions & 23 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ M10 补充:新增 `TeamSchedule` 工具入口,可根据 team objective 为
8181

8282
M10 补充:新增 `TeamAutoSchedule` 工具入口,会在一次调用中先向 running coordinator 注入带成员状态的 objective briefing(如果 team 配置了 coordinator),再为所有 running members 生成 deterministic assignment;这补上了 coordinator briefing 与 member schedule 的一段可验证接线。完整模型驱动 coordinator 决策循环仍未完成。
8383

84+
M10 补充:`TeamAutoSchedule` 现在接受可选 coordinator/model 生成的 `assignments`/`plan`,会把 planned assignment 逐条写入指定 running member,并在 coordinator briefing、structured content 和 progress 中标记 `schedule_source=coordinator_plan`;未提供 plan 时继续走 deterministic assignment。完整后台 coordinator 决策循环仍未完成。
85+
8486
M10 补充:新增 `Sleep` 工具入口,支持 `duration_ms`/`seconds`/Go duration 字符串,最大 60 秒,并使用 tool context cancellation 中断等待;这补齐了 proactive/gated 工具中的安全 wait 原语。完整 ScheduleCron/RemoteTrigger 仍未完成。
8587

8688
M10 补充:新增 `Brief` 工具入口,可把 summary/title/status/details/next_steps/risks 规范为 structured handoff brief,并支持常见字段别名与单字符串列表项归一化;这为后续远端协作/UI brief surface 提供稳定 payload。完整 remote brief UI/调度接线仍未完成。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ test/parity/ # golden tests against TS/official behavior
730730
- 本轮补充:新增 `TeamDispatch` 工具入口,可把不同 assignment message 分别发送给 team 内 running members,并在整批发送前校验所有目标成员归属和 running 状态;这补齐了 broadcast 之外的结构化分派原语。完整自动调度循环仍未完成。
731731
- 本轮补充:新增 `TeamSchedule` 工具入口,可根据 team objective 为每个 running member 生成 deterministic scheduled assignment,消息包含成员序号、当前 team status 和 objective,并在写入前全量校验目标成员 running 状态;完整后台调度循环和模型驱动团队自动调度仍未完成。
732732
- 本轮补充:新增 `TeamAutoSchedule` 工具入口,会在一次调用中先向 running coordinator 注入带成员状态的 objective briefing(如果 team 配置了 coordinator),再为所有 running members 生成 deterministic assignment;这补上了 coordinator briefing 与 member schedule 的一段可验证接线。完整模型驱动 coordinator 决策循环仍未完成。
733+
- 本轮补充:`TeamAutoSchedule` 现在接受可选 coordinator/model 生成的 `assignments`/`plan`,会把 planned assignment 逐条写入指定 running member,并在 coordinator briefing、structured content 和 progress 中标记 `schedule_source=coordinator_plan`;未提供 plan 时继续走 deterministic assignment。完整后台 coordinator 决策循环仍未完成。
733734
- 本轮补充:新增 `Sleep` 工具入口,支持 `duration_ms`/`seconds`/Go duration 字符串,最大 60 秒,并使用 tool context cancellation 中断等待;这补齐了 proactive/gated 工具中的安全 wait 原语。完整 ScheduleCron/RemoteTrigger 仍未完成。
734735
- 本轮补充:新增 `Brief` 工具入口,可把 summary/title/status/details/next_steps/risks 规范为 structured handoff brief,并支持常见字段别名与单字符串列表项归一化;这为后续远端协作/UI brief surface 提供稳定 payload。完整 remote brief UI/调度接线仍未完成。
735736
- 本轮补充:新增 `ScheduleCron` 工具入口和 session-scoped `schedules.json` manifest,可 create/list/delete/trigger/run_due cron schedule metadata,校验 5-field cron 或常见 `@daily` 类表达式,并可绑定 team_id/target/message;`trigger` 会把保存的 schedule message 发送给绑定 team 的 running recipients,`run_due` 会按当前分钟执行到期且启用的 schedule,并记录 last run 状态避免同一分钟重复触发。当前已有手动与一次性到期执行路径,完整后台 daemon 仍未完成。

internal/tools/task/tools.go

Lines changed: 127 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ const (
2222
teamSendTargetCoordinator = "coordinator"
2323
teamSendTargetAll = "all"
2424

25+
teamAutoScheduleSourceDeterministic = "deterministic"
26+
teamAutoScheduleSourceCoordinatorPlan = "coordinator_plan"
27+
2528
scheduleCronActionCreate = "create"
2629
scheduleCronActionList = "list"
2730
scheduleCronActionDelete = "delete"
@@ -93,8 +96,9 @@ type teamScheduleInput struct {
9396
}
9497

9598
type teamAutoScheduleInput struct {
96-
TeamID string `json:"team_id,omitempty"`
97-
Objective string `json:"objective,omitempty"`
99+
TeamID string `json:"team_id,omitempty"`
100+
Objective string `json:"objective,omitempty"`
101+
Assignments []teamDispatchAssignmentInput `json:"assignments,omitempty"`
98102
}
99103

100104
type teamCoordinateInput struct {
@@ -461,8 +465,8 @@ func NewTeamAutoScheduleTool() tool.Tool {
461465
return tool.FuncTool{
462466
DefinitionValue: contracts.ToolDefinition{
463467
Name: "TeamAutoSchedule",
464-
Description: "Send a team objective to the coordinator and deterministically schedule it across running task members.",
465-
SearchHint: "auto schedule objective team coordinator members assignments",
468+
Description: "Send a team objective to the coordinator and schedule it across running task members.",
469+
SearchHint: "auto schedule objective team coordinator model plan members assignments",
466470
ConcurrencySafe: false,
467471
Strict: true,
468472
InputSchema: contracts.JSONSchema{
@@ -471,11 +475,23 @@ func NewTeamAutoScheduleTool() tool.Tool {
471475
"properties": map[string]any{
472476
"team_id": map[string]any{"type": "string"},
473477
"objective": map[string]any{"type": "string"},
478+
"assignments": map[string]any{
479+
"type": "array",
480+
"description": "Optional coordinator/model-produced member plan. When omitted, deterministic assignments are generated for every member.",
481+
"items": map[string]any{
482+
"type": "object",
483+
"required": []any{"task_id", "message"},
484+
"properties": map[string]any{
485+
"task_id": map[string]any{"type": "string"},
486+
"message": map[string]any{"type": "string"},
487+
},
488+
},
489+
},
474490
},
475491
},
476492
},
477493
PromptFunc: func(tool.PromptContext) (string, error) {
478-
return "Combines coordinator briefing and deterministic member scheduling for a subagent team. If the team has a coordinator, the objective is first appended to the running coordinator with team status context; then every running member receives an assignment message.", nil
494+
return "Combines coordinator briefing and member scheduling for a subagent team. If the team has a coordinator, the objective is first appended to the running coordinator with team status context. Provide assignments when a coordinator/model has produced a concrete member plan; otherwise every running member receives a deterministic assignment message.", nil
479495
},
480496
NormalizeFunc: normalizeTeamAutoScheduleInput,
481497
ValidateFunc: validateTeamAutoSchedule,
@@ -1045,7 +1061,7 @@ func validateTeamAutoSchedule(ctx tool.Context, raw json.RawMessage) error {
10451061
if err != nil {
10461062
return err
10471063
}
1048-
taskIDs, err := teamScheduleTaskIDs(team)
1064+
taskIDs, err := teamAutoScheduleTaskIDs(team, input)
10491065
if err != nil {
10501066
return err
10511067
}
@@ -1892,13 +1908,17 @@ func callTeamAutoSchedule(ctx tool.Context, raw json.RawMessage, sink tool.Progr
18921908
if err != nil {
18931909
return contracts.ToolResult{}, err
18941910
}
1895-
taskIDs, err := teamScheduleTaskIDs(team)
1911+
taskIDs, err := teamAutoScheduleTaskIDs(team, input)
18961912
if err != nil {
18971913
return contracts.ToolResult{}, err
18981914
}
18991915
if err := validateTaskIDsRunning(manager, taskIDs); err != nil {
19001916
return contracts.ToolResult{}, err
19011917
}
1918+
scheduleSource := teamAutoScheduleSourceDeterministic
1919+
if len(input.Assignments) > 0 {
1920+
scheduleSource = teamAutoScheduleSourceCoordinatorPlan
1921+
}
19021922
var coordinator session.SidechainState
19031923
if team.CoordinatorTaskID != "" {
19041924
coordinator, err = runningTeamCoordinatorState(manager, team)
@@ -1912,9 +1932,10 @@ func callTeamAutoSchedule(ctx tool.Context, raw json.RawMessage, sink tool.Progr
19121932
structured["type"] = "team_auto_schedule"
19131933
structured["objective"] = input.Objective
19141934
structured["objective_chars"] = len(input.Objective)
1935+
structured["schedule_source"] = scheduleSource
19151936
structured["tasks"] = tasks
19161937
if coordinator.ID != "" {
1917-
briefing := formatTeamCoordinateMessage(team, tasks, input.Objective)
1938+
briefing := formatTeamAutoCoordinateMessage(team, tasks, input.Objective, input.Assignments)
19181939
message, err := appendTaskUserMessage(manager, ctx.SessionID, coordinator.ID, briefing, now)
19191940
if err != nil {
19201941
return contracts.ToolResult{}, err
@@ -1933,20 +1954,39 @@ func callTeamAutoSchedule(ctx tool.Context, raw json.RawMessage, sink tool.Progr
19331954
}
19341955
}
19351956
assignments := make([]map[string]any, 0, len(taskIDs))
1936-
for i, taskID := range taskIDs {
1937-
scheduleMessage := formatTeamScheduleMessage(team, tasks, input.Objective, taskID, i+1, len(taskIDs))
1938-
message, err := appendTaskUserMessage(manager, ctx.SessionID, taskID, scheduleMessage, now)
1939-
if err != nil {
1940-
return contracts.ToolResult{}, err
1957+
if len(input.Assignments) > 0 {
1958+
for _, assignment := range input.Assignments {
1959+
plannedMessage := formatTeamPlannedAssignmentMessage(team, input.Objective, assignment)
1960+
message, err := appendTaskUserMessage(manager, ctx.SessionID, assignment.TaskID, plannedMessage, now)
1961+
if err != nil {
1962+
return contracts.ToolResult{}, err
1963+
}
1964+
assignments = append(assignments, map[string]any{
1965+
"task_id": assignment.TaskID,
1966+
"sidechain_id": assignment.TaskID,
1967+
"message_uuid": string(message.UUID),
1968+
"message_chars": len(assignment.Message),
1969+
"planned_message_chars": len(plannedMessage),
1970+
"schedule_source": scheduleSource,
1971+
})
1972+
}
1973+
} else {
1974+
for i, taskID := range taskIDs {
1975+
scheduleMessage := formatTeamScheduleMessage(team, tasks, input.Objective, taskID, i+1, len(taskIDs))
1976+
message, err := appendTaskUserMessage(manager, ctx.SessionID, taskID, scheduleMessage, now)
1977+
if err != nil {
1978+
return contracts.ToolResult{}, err
1979+
}
1980+
assignments = append(assignments, map[string]any{
1981+
"task_id": taskID,
1982+
"sidechain_id": taskID,
1983+
"member_index": i + 1,
1984+
"member_count": len(taskIDs),
1985+
"message_uuid": string(message.UUID),
1986+
"message_chars": len(scheduleMessage),
1987+
"schedule_source": scheduleSource,
1988+
})
19411989
}
1942-
assignments = append(assignments, map[string]any{
1943-
"task_id": taskID,
1944-
"sidechain_id": taskID,
1945-
"member_index": i + 1,
1946-
"member_count": len(taskIDs),
1947-
"message_uuid": string(message.UUID),
1948-
"message_chars": len(scheduleMessage),
1949-
})
19501990
}
19511991
structured["assignment_count"] = len(assignments)
19521992
structured["assignments"] = assignments
@@ -1957,9 +1997,10 @@ func callTeamAutoSchedule(ctx tool.Context, raw json.RawMessage, sink tool.Progr
19571997
"has_coordinator": coordinator.ID != "",
19581998
"coordinator_task_id": coordinator.ID,
19591999
"objective_chars": len(input.Objective),
2000+
"schedule_source": scheduleSource,
19602001
})
19612002
return contracts.ToolResult{
1962-
Content: fmt.Sprintf("Auto-scheduled %d assignment(s) in team %s.", len(assignments), team.ID),
2003+
Content: fmt.Sprintf("Auto-scheduled %d assignment(s) in team %s using %s.", len(assignments), team.ID, scheduleSource),
19632004
StructuredContent: structured,
19642005
}, nil
19652006
}
@@ -2623,6 +2664,10 @@ func decodeTeamAutoScheduleInput(raw json.RawMessage) (teamAutoScheduleInput, er
26232664
}
26242665
input.TeamID = strings.TrimSpace(input.TeamID)
26252666
input.Objective = strings.TrimSpace(input.Objective)
2667+
for i := range input.Assignments {
2668+
input.Assignments[i].TaskID = sanitizeTaskLikeID(input.Assignments[i].TaskID)
2669+
input.Assignments[i].Message = strings.TrimSpace(input.Assignments[i].Message)
2670+
}
26262671
return input, nil
26272672
}
26282673

@@ -2900,7 +2945,28 @@ func normalizeTeamScheduleInput(raw json.RawMessage) (json.RawMessage, error) {
29002945
}
29012946

29022947
func normalizeTeamAutoScheduleInput(raw json.RawMessage) (json.RawMessage, error) {
2903-
return normalizeTeamScheduleInput(raw)
2948+
obj, err := decodeRawTaskObject(raw)
2949+
if err != nil {
2950+
return nil, err
2951+
}
2952+
for key := range obj {
2953+
switch key {
2954+
case "team_id", "teamId", "id", "name", "objective", "message", "text", "content", "prompt", "input", "instruction", "request", "goal", "assignments", "tasks", "dispatches", "plan", "member_assignments", "memberAssignments", "messages":
2955+
default:
2956+
return nil, fmt.Errorf("input.%s is not allowed", key)
2957+
}
2958+
}
2959+
normalized := map[string]json.RawMessage{}
2960+
if value, ok := firstRawTaskField(obj, "team_id", "teamId", "id", "name"); ok {
2961+
normalized["team_id"] = value
2962+
}
2963+
if value, ok := firstRawTaskField(obj, "objective", "message", "text", "content", "prompt", "input", "instruction", "request", "goal"); ok {
2964+
normalized["objective"] = value
2965+
}
2966+
if value, ok := firstRawTaskField(obj, "assignments", "tasks", "dispatches", "plan", "member_assignments", "memberAssignments", "messages"); ok {
2967+
normalized["assignments"] = value
2968+
}
2969+
return json.Marshal(normalized)
29042970
}
29052971

29062972
func normalizeTeamCoordinateInput(raw json.RawMessage) (json.RawMessage, error) {
@@ -3393,6 +3459,16 @@ func teamDispatchTaskIDs(team session.TeamState, assignments []teamDispatchAssig
33933459
return taskIDs, nil
33943460
}
33953461

3462+
func teamAutoScheduleTaskIDs(team session.TeamState, input teamAutoScheduleInput) ([]string, error) {
3463+
if len(input.Assignments) == 0 {
3464+
return teamScheduleTaskIDs(team)
3465+
}
3466+
if len(input.Assignments) > 32 {
3467+
return nil, fmt.Errorf("assignments must include <= 32 items")
3468+
}
3469+
return teamDispatchTaskIDs(team, input.Assignments)
3470+
}
3471+
33963472
func teamScheduleTaskIDs(team session.TeamState) ([]string, error) {
33973473
if len(team.TaskIDs) == 0 {
33983474
return nil, fmt.Errorf("team %s has no tasks", team.ID)
@@ -3681,6 +3757,20 @@ func formatTeamCoordinateMessage(team session.TeamState, tasks []map[string]any,
36813757
return builder.String()
36823758
}
36833759

3760+
func formatTeamAutoCoordinateMessage(team session.TeamState, tasks []map[string]any, objective string, assignments []teamDispatchAssignmentInput) string {
3761+
briefing := formatTeamCoordinateMessage(team, tasks, objective)
3762+
if len(assignments) == 0 {
3763+
return briefing
3764+
}
3765+
var builder strings.Builder
3766+
builder.WriteString(briefing)
3767+
builder.WriteString("\nPlanned assignments:")
3768+
for _, assignment := range assignments {
3769+
fmt.Fprintf(&builder, "\n- %s: %s", assignment.TaskID, assignment.Message)
3770+
}
3771+
return builder.String()
3772+
}
3773+
36843774
func formatTeamScheduleMessage(team session.TeamState, tasks []map[string]any, objective string, taskID string, index int, total int) string {
36853775
var builder strings.Builder
36863776
fmt.Fprintf(&builder, "Team scheduled assignment for %s.", team.ID)
@@ -3708,6 +3798,20 @@ func formatTeamScheduleMessage(team session.TeamState, tasks []map[string]any, o
37083798
return builder.String()
37093799
}
37103800

3801+
func formatTeamPlannedAssignmentMessage(team session.TeamState, objective string, assignment teamDispatchAssignmentInput) string {
3802+
var builder strings.Builder
3803+
fmt.Fprintf(&builder, "Team planned assignment for %s.", team.ID)
3804+
if team.Description != "" {
3805+
fmt.Fprintf(&builder, "\nDescription: %s", team.Description)
3806+
}
3807+
builder.WriteString("\nObjective:\n")
3808+
builder.WriteString(objective)
3809+
builder.WriteString("\nAssignment:\n")
3810+
builder.WriteString(assignment.Message)
3811+
builder.WriteString("\nInstruction:\nExecute this coordinator/model plan for your assigned scope. Use team communication tools when coordination is needed.")
3812+
return builder.String()
3813+
}
3814+
37113815
func formatTeamDispatchMessage(team session.TeamState, assignment teamDispatchAssignmentInput) string {
37123816
var builder strings.Builder
37133817
fmt.Fprintf(&builder, "Team dispatch assignment for %s.", team.ID)

0 commit comments

Comments
 (0)