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 }