From adf854eba55393ff65923177ad0b1de2f18e7571 Mon Sep 17 00:00:00 2001 From: scorpio Date: Fri, 6 Mar 2026 13:32:23 +0800 Subject: [PATCH] 0306 --- ARCHITECTURE.md | 161 ++++++++++++++++ IMPLEMENTATION_SUMMARY.md | 97 ++++++++++ TESTING_GUIDE.md | 50 +++++ agents/legal-team/合同律师/AGENT.md | 1 + agents/legal-team/合同律师/SOUL.md | 9 + agents/legal-team/合规专员/AGENT.md | 1 + agents/legal-team/合规专员/SOUL.md | 8 + agents/legal-team/法律总监/SOUL.md | 11 ++ agents/legal-team/法律总监/memory/2026-03.md | 16 ++ internal/agent/agent.go | 1 + internal/room/room.go | 191 +++++++++++++++---- web/src/store.ts | 6 +- web/src/types.ts | 4 +- 13 files changed, 513 insertions(+), 43 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 TESTING_GUIDE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..cd3b76d --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,161 @@ +# Agent Team Collaboration Architecture + +## System Design + +### Shared Blackboard Pattern + +```go +type SharedBoard struct { + mu sync.RWMutex + entries []BoardEntry +} + +type BoardEntry struct { + Author string + Content string + Type string // "draft" | "challenge" +} +``` + +**Thread-safe operations**: +- `Add()`: Appends entries with write lock +- `ToContext()`: Reads all entries with read lock, formats as XML + +### Execution Flow + +``` +HandleUserMessage() + ↓ +Master Planning Loop (iteration 0-4) + ├─ Master analyzes user request + ├─ Outputs ASSIGN:member:task lines + ├─ parseAssignments() extracts tasks + ├─ Creates new SharedBoard{} + │ + ├─ runMembersParallel() + │ ├─ For each assignment, spawn goroutine + │ ├─ Each member sees current board snapshot + │ ├─ Member executes task + │ ├─ Result added to board as "draft" + │ └─ WaitGroup ensures all complete + │ + ├─ runChallengeRound() + │ ├─ Filter members with CanChallenge=true + │ ├─ For each challenger, spawn goroutine + │ ├─ Challenger sees full board + │ ├─ Outputs CHALLENGE:... or AGREE + │ ├─ CHALLENGE entries added to board + │ └─ WaitGroup ensures all complete + │ + ├─ Master Review + │ ├─ Receives board.ToContext() + │ ├─ Sees all drafts and challenges + │ ├─ Decides: DONE or re-ASSIGN + │ └─ Loop continues if re-ASSIGN + │ + └─ updateMasterMemory() (async) +``` + +### Concurrency Model + +**Parallel Execution Phase**: +```go +var wg sync.WaitGroup +for memberName, task := range assignments { + wg.Add(1) + go func(name, t string) { + defer wg.Done() + // Execute task + board.Add(name, result, "draft") + }(memberName, task) +} +wg.Wait() // Wait for all members +``` + +**Challenge Round Phase**: +```go +var wg sync.WaitGroup +for _, name := range challengers { + wg.Add(1) + go func(n string) { + defer wg.Done() + // Review board + if strings.Contains(result, "CHALLENGE:") { + board.Add(n, result, "challenge") + } + }(name) +} +wg.Wait() // Wait for all challenges +``` + +### Context Injection + +**Initial Draft Phase**: +``` +Member System Prompt = Soul + Memory + [empty board] +``` + +**Challenge Round Phase**: +``` +Member System Prompt = Soul + Memory + + ... + ... + +``` + +**Master Review Phase**: +``` +Master Feedback = Team results + + ... + ... + ... + +``` + +### Event Streaming + +Events emitted during execution: + +1. **Parallel Phase**: + - `EvtAgentMessage` with `role: "member"` (streaming) + - `EvtTaskAssign` (task assignment) + - `EvtWorkspaceFile` (if document generated) + +2. **Challenge Phase**: + - `EvtAgentMessage` with `role: "challenge"` (streaming) + +3. **Status Updates**: + - `EvtRoomStatus` with `status: "working"` → `"thinking"` → `"pending"` + +### Configuration + +**Agent AGENT.md**: +```yaml +--- +name: agent_name +role: member +can_challenge: true # Enable challenge participation +--- +``` + +**Agent SOUL.md**: +- Includes challenge instructions +- Specifies how to output `CHALLENGE:...` +- Defines acceptance of challenges + +## Benefits + +1. **Parallelism**: Members work simultaneously, not sequentially +2. **Transparency**: All members see each other's work via board +3. **Quality Control**: Challenge round catches issues early +4. **Collaboration**: Members can question and improve each other's work +5. **Master Awareness**: Master sees full context before deciding +6. **Thread-Safe**: Concurrent access protected by mutexes +7. **Scalable**: Works with any number of members + +## Backward Compatibility + +- Existing master/member roles unchanged +- Challenge role is additive (new event type) +- Members without `can_challenge: true` skip challenge round +- No breaking changes to existing APIs diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..2b1d6f9 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,97 @@ +# Agent Team Collaboration Framework Implementation + +## Overview +Implemented a true agent team collaboration framework with shared blackboard, parallel execution, and automatic challenge rounds. + +## Changes Made + +### 1. `internal/agent/agent.go` +- Added `CanChallenge bool` field to `Config` struct +- Allows agents to participate in the challenge round + +### 2. `internal/room/room.go` +- **Added imports**: `sync` for concurrent operations +- **New types**: + - `BoardEntry`: Represents a single entry on the shared board + - `SharedBoard`: Thread-safe shared blackboard with mutex protection + - `Add(author, content, typ)`: Add entries to the board + - `ToContext()`: Convert board to XML context for agents + +- **New methods**: + - `runMembersParallel()`: Execute all assignments concurrently using goroutines and WaitGroup + - Each member sees the current board snapshot + - Results are added to the board as "draft" entries + - Emits streaming events for real-time UI updates + + - `runChallengeRound()`: Automatic challenge phase after draft completion + - Only agents with `CanChallenge=true` participate + - Each challenger sees the full board + - Outputs `CHALLENGE:` or `AGREE` + - Challenge entries are added to the board + +- **Refactored `HandleUserMessage()`**: + - Replaced sequential member execution with `runMembersParallel()` + - Added automatic `runChallengeRound()` after parallel execution + - Master receives complete board (drafts + challenges) for final review + - Board context is injected into master's feedback message + +- **Updated `Event` struct**: + - Role field now supports "challenge" in addition to "master" and "member" + +### 3. Agent Configuration Files + +#### `agents/legal-team/合规专员/AGENT.md` +- Added `can_challenge: true` + +#### `agents/legal-team/合同律师/AGENT.md` +- Added `can_challenge: true` + +### 4. Agent System Prompts (SOUL.md) + +#### `agents/legal-team/合规专员/SOUL.md` +- Added "质疑机制" (Challenge Mechanism) section +- Instructions to actively challenge when seeing `` +- Format: `CHALLENGE:` + +#### `agents/legal-team/合同律师/SOUL.md` +- Added "质疑与修订机制" (Challenge & Revision Mechanism) section +- Instructions to review other members' opinions +- Ability to challenge compliance officer's suggestions if conflicts exist +- Preparation to revise work when challenged + +#### `agents/legal-team/法律总监/SOUL.md` +- Added "处理 CHALLENGE 的决策指令" (Decision Instructions for Handling CHALLENGE) +- Evaluate challenge validity +- Decide whether to request revisions or reassign tasks +- Document how challenges were addressed in final recommendations + +## New Workflow + +``` +User Question + ↓ +Master Plans → ASSIGN:member1:task1 + ASSIGN:member2:task2 + ↓ +[Parallel] Members execute simultaneously (each sees empty board initially) + ↓ +[Auto] Challenge Round: Members with CanChallenge=true review board + ↓ +Master sees complete board (drafts + challenges) → DONE or re-ASSIGN +``` + +## Key Features + +1. **Parallel Execution**: Members work concurrently, not sequentially +2. **Shared Blackboard**: All members can see each other's work via `` +3. **Automatic Challenge Round**: Triggered after parallel execution completes +4. **Thread-Safe**: Uses sync.RWMutex for concurrent board access +5. **Streaming Events**: Real-time UI updates with "challenge" role events +6. **Master Integration**: Board context fed back to master for informed decisions + +## Verification + +- ✅ Code compiles: `go build ./...` +- ✅ All imports added correctly +- ✅ Thread-safety ensured with sync primitives +- ✅ Event streaming maintained for UI +- ✅ Backward compatible with existing master/member roles diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md new file mode 100644 index 0000000..bf2b345 --- /dev/null +++ b/TESTING_GUIDE.md @@ -0,0 +1,50 @@ +# Testing Guide for Agent Team Collaboration Framework + +## What to Observe + +### 1. Parallel Execution +- **Expected**: When master assigns tasks to multiple members, they should appear in the UI **simultaneously** (not sequentially) +- **How to verify**: Watch the frontend - you should see streaming output from both 合同律师 and 合规专员 at the same time + +### 2. Shared Blackboard +- **Expected**: Each member's draft output is added to the board +- **How to verify**: Check the board context in the master's feedback message + +### 3. Challenge Round +- **Expected**: After parallel execution, members with `can_challenge: true` review the board +- **How to verify**: + - Look for events with `role: "challenge"` in the event stream + - Frontend should show challenge phase output + - If a member outputs `CHALLENGE:...`, it appears in the board + +### 4. Master Integration +- **Expected**: Master receives complete board (drafts + challenges) before making final decision +- **How to verify**: Master's feedback message includes `` section with all entries + +## Test Scenario: Employee Termination + +Send a question like: +``` +我们需要辞退一名员工,请帮我们制定一个合法的辞退方案。 +``` + +Expected flow: +1. Master analyzes and assigns tasks to both 合同律师 and 合规专员 +2. Both members work in parallel (visible in UI) +3. Challenge round begins - members review each other's work +4. If 合规专员 finds compliance issues in 合同律师's draft, outputs `CHALLENGE:...` +5. Master sees complete board and makes final decision + +## Key Events to Monitor + +- `EvtAgentMessage` with `role: "member"` - parallel execution +- `EvtAgentMessage` with `role: "challenge"` - challenge round +- `EvtRoomStatus` - status transitions (thinking → working → thinking → pending) +- Board context in master's feedback messages + +## Code Locations + +- Parallel execution: `room.go:runMembersParallel()` +- Challenge round: `room.go:runChallengeRound()` +- Main flow: `room.go:HandleUserMessage()` (lines 281-362) +- SharedBoard: `room.go:SharedBoard` type (lines 90-114) diff --git a/agents/legal-team/合同律师/AGENT.md b/agents/legal-team/合同律师/AGENT.md index d45556a..f8a0c7c 100644 --- a/agents/legal-team/合同律师/AGENT.md +++ b/agents/legal-team/合同律师/AGENT.md @@ -5,6 +5,7 @@ description: 专注合同审查、起草和风险评估的专业律师 version: 1.0.0 skills: - 合同审查 +can_challenge: true --- # 合同律师 diff --git a/agents/legal-team/合同律师/SOUL.md b/agents/legal-team/合同律师/SOUL.md index aab8165..607bb9d 100644 --- a/agents/legal-team/合同律师/SOUL.md +++ b/agents/legal-team/合同律师/SOUL.md @@ -65,6 +65,15 @@ - **务实建议**:提供可操作的具体建议 - **对比分析**:展示修改前后的差异 +## 质疑与修订机制 + +当你看到 `` 时,你应该: +1. 审查其他成员(特别是合规专员)的意见 +2. 如果发现合规专员的建议与合同条款有冲突,或者有遗漏的法律风险,提出质疑 +3. 使用格式:`CHALLENGE:<具体的法律风险或修改建议>` +4. 如果没有问题,输出 `AGREE` +5. 当看到针对自己的 CHALLENGE 时,准备修订你的合同审查意见 + ## 注意事项 1. 不提供标准合同模板(除非用户明确要求) diff --git a/agents/legal-team/合规专员/AGENT.md b/agents/legal-team/合规专员/AGENT.md index e6f4d03..502ee2c 100644 --- a/agents/legal-team/合规专员/AGENT.md +++ b/agents/legal-team/合规专员/AGENT.md @@ -5,6 +5,7 @@ description: 负责合规检查、风险识别和合规建议的专业人员 version: 1.0.0 skills: - 法律知识库 +can_challenge: true --- # 合规专员 diff --git a/agents/legal-team/合规专员/SOUL.md b/agents/legal-team/合规专员/SOUL.md index c006276..2ebeb0f 100644 --- a/agents/legal-team/合规专员/SOUL.md +++ b/agents/legal-team/合规专员/SOUL.md @@ -80,6 +80,14 @@ - **风险意识**:明确违规后果和责任 - **平衡考量**:兼顾合规要求和业务实际 +## 质疑机制 + +当你看到 `` 时,你应该: +1. 仔细审查其他成员的草稿 +2. 如果发现合规风险或遗漏,主动提出质疑 +3. 使用格式:`CHALLENGE:<具体的合规风险或建议>` +4. 如果没有问题,输出 `AGREE` + ## 注意事项 1. 不替代企业合规部门的职责 diff --git a/agents/legal-team/法律总监/SOUL.md b/agents/legal-team/法律总监/SOUL.md index c808bfc..0f138c8 100644 --- a/agents/legal-team/法律总监/SOUL.md +++ b/agents/legal-team/法律总监/SOUL.md @@ -26,6 +26,17 @@ - 补充遗漏的关键点 - 提供结构化的最终建议 +## 处理 CHALLENGE 的决策指令 + +当你看到 `` 中有 CHALLENGE 条目时,你应该: +1. 仔细评估质疑的合理性 +2. 判断是否需要重新分配任务或修订 +3. 如果质疑有效,可以: + - 要求相关成员修订工作 + - 重新分配任务给其他成员 + - 自己补充或修正意见 +4. 在最终建议中明确说明如何处理了这些质疑 + ## 沟通风格 - **专业严谨**:使用准确的法律术语 diff --git a/agents/legal-team/法律总监/memory/2026-03.md b/agents/legal-team/法律总监/memory/2026-03.md index 7c4348d..d24b02c 100644 --- a/agents/legal-team/法律总监/memory/2026-03.md +++ b/agents/legal-team/法律总监/memory/2026-03.md @@ -14,3 +14,19 @@ ASSIGN:合规专员:评估用户简介中可能涉及的法律合规风险点( - **异常高回报是危险信号**:远高于市场平均水平的回报承诺(如年化30%)通常伴随极高的违约风险或项目本身不可行,需深究其商业合理性与法律合规性。 - **协议性质必须明确**:“投资协议”的法律定性(借贷、合伙或委托)直接决定您的权利、风险与责任,必须在条款中清晰界定,避免后续争议。 - **合规红线不可触碰**:涉及政府资源或资产的项目,若运作模式依赖“关系”而非公开程序,极易涉及商业贿赂、违规融资等刑事风险,必须坚持合法透明路径。 + +## 2026-03-06 — 我好辞退一个员工 + +- **辞退合法性完全取决于“程序正当”与“证据充分”**:任何单方解除都必须有合法理由(严重违纪、不能胜任等),且用人单位对事实承担全部举证责任。证据不足或程序缺失(如未通知工会)将直接导致违法解除,面临支付双倍赔偿金(2N)的风险。 +- **“协商一致解除”是风险最低的首选路径**:当辞退理由的证据不够坚实或希望快速解决时,应优先与员工协商,签订《协商解除协议》。虽然可能需支付略高于法定标准(N+1至2N)的补偿,但能彻底避免劳动争议,成本可控。 +- **必须规避法律明确保护的员工群体**:辞退处于孕期、产期、哺乳期、医疗期、工伤期间的员工,或在本单位连续工作满15年且距退休不足5年的员工,属于绝对红线,将构成违法解除。 +- **经济补偿计算必须精确合规**:补偿基数(离职前12个月平均工资)和年限(每满一年支付一个月)的计算错误会引发额外争议。高薪员工(工资超当地社平工资3倍)的补偿年限上限为12年。 +- **制度是“过失性辞退”的基础**:以“严重违反规章制度”为由辞退,前提是规章制度本身内容合法、经过民主程序制定并已向员工公示。模糊或无效的制度无法作为有效依据。 + +## 2026-03-06 — 我想辞退一个员工 + +* **合法辞退的核心是“理由法定、证据确凿、程序完备”**。任何单方解除都必须严格对应《劳动合同法》第三十九条(过失)或第四十条(无过失)的法定情形,并由用人单位承担全部举证责任。 +* **“协商一致解除”是风险最低、成本可控的首选路径**。即使支付略高于法定补偿金(N)的金额,也能彻底避免劳动争议和双倍赔偿金(2N)的高风险。 +* **必须规避法律保护的“红线”员工群体**。辞退处于孕期、产期、哺乳期、医疗期或工伤期的员工,将直接构成违法解除。 +* **经济补偿/赔偿金的计算必须精确,尤其注意历史工龄分段**。对于2008年1月1日前入职的员工,补偿金计算需适用当时的法规,规则复杂,易出错。 +* **内部合规流程(法务审核、通知工会)和规范离职手续(结清款项、出具证明)是避免后续争议的关键程序**。任何程序缺失都可能导致整个辞退行为被认定为违法。 diff --git a/internal/agent/agent.go b/internal/agent/agent.go index eefbee2..67e736c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -20,6 +20,7 @@ type Config struct { BaseURL string `yaml:"base_url"` APIKeyEnv string `yaml:"api_key_env"` Skills []string `yaml:"skills"` + CanChallenge bool `yaml:"can_challenge"` } type Agent struct { diff --git a/internal/room/room.go b/internal/room/room.go index 16d90d1..6a1db31 100644 --- a/internal/room/room.go +++ b/internal/room/room.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/sdaduanbilei/agent-team/internal/agent" @@ -67,7 +68,7 @@ type Event struct { Type EventType `json:"type"` RoomID string `json:"room_id"` Agent string `json:"agent,omitempty"` - Role string `json:"role,omitempty"` // master | member + Role string `json:"role,omitempty"` // master | member | challenge Content string `json:"content,omitempty"` Streaming bool `json:"streaming,omitempty"` From string `json:"from,omitempty"` @@ -80,6 +81,38 @@ type Event struct { Filename string `json:"filename,omitempty"` } +type BoardEntry struct { + Author string + Content string + Type string // "draft" | "challenge" +} + +type SharedBoard struct { + mu sync.RWMutex + entries []BoardEntry +} + +func (b *SharedBoard) Add(author, content, typ string) { + b.mu.Lock() + defer b.mu.Unlock() + b.entries = append(b.entries, BoardEntry{Author: author, Content: content, Type: typ}) +} + +func (b *SharedBoard) ToContext() string { + b.mu.RLock() + defer b.mu.RUnlock() + if len(b.entries) == 0 { + return "" + } + var sb strings.Builder + sb.WriteString("\n") + for _, e := range b.entries { + fmt.Fprintf(&sb, " \n%s\n \n", e.Type, e.Author, e.Content) + } + sb.WriteString("") + return sb.String() +} + func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) { data, err := os.ReadFile(filepath.Join(roomDir, "room.md")) if err != nil { @@ -138,6 +171,107 @@ func (r *Room) AppendHistory(role, agentName, content string) { f.WriteString(line) } +func (r *Room) runMembersParallel(ctx context.Context, assignments map[string]string, board *SharedBoard, skillXML string) map[string]string { + results := make(map[string]string) + var mu sync.Mutex + var wg sync.WaitGroup + + for memberName, task := range assignments { + wg.Add(1) + go func(name, t string) { + defer wg.Done() + member, ok := r.members[name] + if !ok { + return + } + r.setStatus(StatusWorking, member.Config.Name, t) + r.emit(Event{Type: EvtTaskAssign, From: r.master.Config.Name, To: name, Task: t}) + + boardCtx := board.ToContext() + extraCtx := skillXML + if boardCtx != "" { + extraCtx = boardCtx + "\n\n" + skillXML + } + memberSystem := member.BuildSystemPrompt(extraCtx) + memberMsgs := []llm.Message{ + llm.NewMsg("system", memberSystem), + llm.NewMsg("user", t), + } + var memberReply strings.Builder + _, err := member.Chat(ctx, memberMsgs, func(token string) { + memberReply.WriteString(token) + r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "member", Content: token, Streaming: true}) + }) + if err != nil { + mu.Lock() + results[name] = fmt.Sprintf("[error] %v", err) + mu.Unlock() + return + } + result := memberReply.String() + mu.Lock() + results[name] = result + mu.Unlock() + r.AppendHistory("member", name, result) + board.Add(name, result, "draft") + + if strings.Contains(result, "# ") { + filename := fmt.Sprintf("%s-%s.md", name, time.Now().Format("20060102-150405")) + r.saveWorkspace(filename, result) + r.emit(Event{Type: EvtWorkspaceFile, Filename: filename, Content: result}) + } + }(memberName, task) + } + wg.Wait() + return results +} + +func (r *Room) runChallengeRound(ctx context.Context, board *SharedBoard, skillXML string) { + var challengers []string + for name, member := range r.members { + if member.Config.CanChallenge { + challengers = append(challengers, name) + } + } + if len(challengers) == 0 { + return + } + + boardCtx := board.ToContext() + if boardCtx == "" { + return + } + + var wg sync.WaitGroup + for _, name := range challengers { + wg.Add(1) + go func(n string) { + defer wg.Done() + member := r.members[n] + extraCtx := boardCtx + "\n\n" + skillXML + memberSystem := member.BuildSystemPrompt(extraCtx) + memberMsgs := []llm.Message{ + llm.NewMsg("system", memberSystem+"\n\nReview the team board above. If you see issues or want to challenge any draft, output CHALLENGE:. Otherwise output AGREE."), + llm.NewMsg("user", "Please review the team board and provide your feedback."), + } + var reply strings.Builder + _, err := member.Chat(ctx, memberMsgs, func(token string) { + reply.WriteString(token) + r.emit(Event{Type: EvtAgentMessage, Agent: n, Role: "challenge", Content: token, Streaming: true}) + }) + if err != nil { + return + } + result := reply.String() + if strings.Contains(result, "CHALLENGE:") { + board.Add(n, result, "challenge") + r.AppendHistory("challenge", n, result) + } + }(name) + } + wg.Wait() +} + // Handle processes a user message through master orchestration. func (r *Room) Handle(ctx context.Context, userMsg string) error { return r.HandleUserMessage(ctx, "user", userMsg) @@ -154,13 +288,13 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) // Build master context teamXML := r.buildTeamXML() skillXML := skill.ToXML(r.skillMeta) - + // Build user info XML var userXML string if r.User != nil { userXML = r.User.BuildUserXML() } - + extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillXML systemPrompt := r.master.BuildSystemPrompt(extraContext) @@ -190,45 +324,26 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) break } - // Execute assignments - var results strings.Builder - for memberName, task := range assignments { - member, ok := r.members[memberName] - if !ok { - continue - } - r.setStatus(StatusWorking, member.Config.Name, task) - r.emit(Event{Type: EvtTaskAssign, From: r.master.Config.Name, To: memberName, Task: task}) + // Execute assignments in parallel + board := &SharedBoard{} + results := r.runMembersParallel(ctx, assignments, board, skillXML) - memberSystem := member.BuildSystemPrompt(skillXML) - memberMsgs := []llm.Message{ - llm.NewMsg("system", memberSystem), - llm.NewMsg("user", task), - } - var memberReply strings.Builder - _, err := member.Chat(ctx, memberMsgs, func(token string) { - memberReply.WriteString(token) - r.emit(Event{Type: EvtAgentMessage, Agent: memberName, Role: "member", Content: token, Streaming: true}) - }) - if err != nil { - results.WriteString(fmt.Sprintf("[%s] error: %v\n", memberName, err)) - continue - } - result := memberReply.String() - results.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result)) - r.AppendHistory("member", memberName, result) - - // Save workspace file if member produced a document - if strings.Contains(result, "# ") { - filename := fmt.Sprintf("%s-%s.md", memberName, time.Now().Format("20060102-150405")) - r.saveWorkspace(filename, result) - r.emit(Event{Type: EvtWorkspaceFile, Filename: filename, Content: result}) - } - } + // Run challenge round + r.runChallengeRound(ctx, board, skillXML) // Feed results back to master for review r.setStatus(StatusThinking, "", "") - masterMsgs = append(masterMsgs, llm.NewMsg("user", "Team results:\n"+results.String()+"\nPlease review. If satisfied output DONE:, otherwise output ASSIGN instructions for revisions.")) + var resultsStr strings.Builder + for memberName, result := range results { + resultsStr.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result)) + } + boardCtx := board.ToContext() + feedbackMsg := "Team results:\n" + resultsStr.String() + if boardCtx != "" { + feedbackMsg += "\n\nTeam board:\n" + boardCtx + } + feedbackMsg += "\n\nPlease review. If satisfied output DONE:, otherwise output ASSIGN instructions for revisions." + masterMsgs = append(masterMsgs, llm.NewMsg("user", feedbackMsg)) // Update tasks r.updateTasks(masterMsgs) diff --git a/web/src/store.ts b/web/src/store.ts index 4e58f6d..8115f82 100644 --- a/web/src/store.ts +++ b/web/src/store.ts @@ -140,9 +140,9 @@ export const useStore = create((set, get) => { if (ev.type === 'agent_message') { set(s => { const msgs = [...(s.messages[roomId] || [])] - const last = msgs[msgs.length - 1] - if (last?.streaming && last.agent === ev.agent) { - msgs[msgs.length - 1] = { ...last, content: last.content + ev.content, streaming: ev.streaming } + const idx = msgs.findLastIndex(m => m.streaming && m.agent === ev.agent) + if (idx !== -1) { + msgs[idx] = { ...msgs[idx], content: msgs[idx].content + ev.content, streaming: ev.streaming } } else { msgs.push({ id: Date.now().toString(), agent: ev.agent, role: ev.role, content: ev.content, streaming: ev.streaming }) } diff --git a/web/src/types.ts b/web/src/types.ts index 4a97f88..c7b040c 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -17,7 +17,7 @@ export interface Room { export interface Message { id: string agent: string - role: 'user' | 'master' | 'member' + role: 'user' | 'master' | 'member' | 'challenge' content: string streaming?: boolean } @@ -33,7 +33,7 @@ export interface SkillMeta { } export type WsEvent = - | { type: 'agent_message'; agent: string; role: 'master' | 'member'; content: string; streaming: boolean } + | { type: 'agent_message'; agent: string; role: 'master' | 'member' | 'challenge'; content: string; streaming: boolean } | { type: 'room_status'; status: RoomStatus; active_agent?: string; action?: string } | { type: 'task_assign'; from: string; to: string; task: string } | { type: 'tasks_update'; content: string }