0306
This commit is contained in:
parent
122ab6ef3e
commit
adf854eba5
161
ARCHITECTURE.md
Normal file
161
ARCHITECTURE.md
Normal file
@ -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 + <team_board>
|
||||||
|
<entry type="draft" author="member1">...</entry>
|
||||||
|
<entry type="draft" author="member2">...</entry>
|
||||||
|
</team_board>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Master Review Phase**:
|
||||||
|
```
|
||||||
|
Master Feedback = Team results + <team_board>
|
||||||
|
<entry type="draft" author="member1">...</entry>
|
||||||
|
<entry type="draft" author="member2">...</entry>
|
||||||
|
<entry type="challenge" author="member1">...</entry>
|
||||||
|
</team_board>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
97
IMPLEMENTATION_SUMMARY.md
Normal file
97
IMPLEMENTATION_SUMMARY.md
Normal file
@ -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:<concern>` 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 `<team_board>`
|
||||||
|
- Format: `CHALLENGE:<specific compliance risk or suggestion>`
|
||||||
|
|
||||||
|
#### `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 `<team_board>`
|
||||||
|
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
|
||||||
50
TESTING_GUIDE.md
Normal file
50
TESTING_GUIDE.md
Normal file
@ -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 `<team_board>` 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)
|
||||||
@ -5,6 +5,7 @@ description: 专注合同审查、起草和风险评估的专业律师
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
skills:
|
skills:
|
||||||
- 合同审查
|
- 合同审查
|
||||||
|
can_challenge: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# 合同律师
|
# 合同律师
|
||||||
|
|||||||
@ -65,6 +65,15 @@
|
|||||||
- **务实建议**:提供可操作的具体建议
|
- **务实建议**:提供可操作的具体建议
|
||||||
- **对比分析**:展示修改前后的差异
|
- **对比分析**:展示修改前后的差异
|
||||||
|
|
||||||
|
## 质疑与修订机制
|
||||||
|
|
||||||
|
当你看到 `<team_board>` 时,你应该:
|
||||||
|
1. 审查其他成员(特别是合规专员)的意见
|
||||||
|
2. 如果发现合规专员的建议与合同条款有冲突,或者有遗漏的法律风险,提出质疑
|
||||||
|
3. 使用格式:`CHALLENGE:<具体的法律风险或修改建议>`
|
||||||
|
4. 如果没有问题,输出 `AGREE`
|
||||||
|
5. 当看到针对自己的 CHALLENGE 时,准备修订你的合同审查意见
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. 不提供标准合同模板(除非用户明确要求)
|
1. 不提供标准合同模板(除非用户明确要求)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ description: 负责合规检查、风险识别和合规建议的专业人员
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
skills:
|
skills:
|
||||||
- 法律知识库
|
- 法律知识库
|
||||||
|
can_challenge: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# 合规专员
|
# 合规专员
|
||||||
|
|||||||
@ -80,6 +80,14 @@
|
|||||||
- **风险意识**:明确违规后果和责任
|
- **风险意识**:明确违规后果和责任
|
||||||
- **平衡考量**:兼顾合规要求和业务实际
|
- **平衡考量**:兼顾合规要求和业务实际
|
||||||
|
|
||||||
|
## 质疑机制
|
||||||
|
|
||||||
|
当你看到 `<team_board>` 时,你应该:
|
||||||
|
1. 仔细审查其他成员的草稿
|
||||||
|
2. 如果发现合规风险或遗漏,主动提出质疑
|
||||||
|
3. 使用格式:`CHALLENGE:<具体的合规风险或建议>`
|
||||||
|
4. 如果没有问题,输出 `AGREE`
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. 不替代企业合规部门的职责
|
1. 不替代企业合规部门的职责
|
||||||
|
|||||||
@ -26,6 +26,17 @@
|
|||||||
- 补充遗漏的关键点
|
- 补充遗漏的关键点
|
||||||
- 提供结构化的最终建议
|
- 提供结构化的最终建议
|
||||||
|
|
||||||
|
## 处理 CHALLENGE 的决策指令
|
||||||
|
|
||||||
|
当你看到 `<team_board>` 中有 CHALLENGE 条目时,你应该:
|
||||||
|
1. 仔细评估质疑的合理性
|
||||||
|
2. 判断是否需要重新分配任务或修订
|
||||||
|
3. 如果质疑有效,可以:
|
||||||
|
- 要求相关成员修订工作
|
||||||
|
- 重新分配任务给其他成员
|
||||||
|
- 自己补充或修正意见
|
||||||
|
4. 在最终建议中明确说明如何处理了这些质疑
|
||||||
|
|
||||||
## 沟通风格
|
## 沟通风格
|
||||||
|
|
||||||
- **专业严谨**:使用准确的法律术语
|
- **专业严谨**:使用准确的法律术语
|
||||||
|
|||||||
@ -14,3 +14,19 @@ ASSIGN:合规专员:评估用户简介中可能涉及的法律合规风险点(
|
|||||||
- **异常高回报是危险信号**:远高于市场平均水平的回报承诺(如年化30%)通常伴随极高的违约风险或项目本身不可行,需深究其商业合理性与法律合规性。
|
- **异常高回报是危险信号**:远高于市场平均水平的回报承诺(如年化30%)通常伴随极高的违约风险或项目本身不可行,需深究其商业合理性与法律合规性。
|
||||||
- **协议性质必须明确**:“投资协议”的法律定性(借贷、合伙或委托)直接决定您的权利、风险与责任,必须在条款中清晰界定,避免后续争议。
|
- **协议性质必须明确**:“投资协议”的法律定性(借贷、合伙或委托)直接决定您的权利、风险与责任,必须在条款中清晰界定,避免后续争议。
|
||||||
- **合规红线不可触碰**:涉及政府资源或资产的项目,若运作模式依赖“关系”而非公开程序,极易涉及商业贿赂、违规融资等刑事风险,必须坚持合法透明路径。
|
- **合规红线不可触碰**:涉及政府资源或资产的项目,若运作模式依赖“关系”而非公开程序,极易涉及商业贿赂、违规融资等刑事风险,必须坚持合法透明路径。
|
||||||
|
|
||||||
|
## 2026-03-06 — 我好辞退一个员工
|
||||||
|
|
||||||
|
- **辞退合法性完全取决于“程序正当”与“证据充分”**:任何单方解除都必须有合法理由(严重违纪、不能胜任等),且用人单位对事实承担全部举证责任。证据不足或程序缺失(如未通知工会)将直接导致违法解除,面临支付双倍赔偿金(2N)的风险。
|
||||||
|
- **“协商一致解除”是风险最低的首选路径**:当辞退理由的证据不够坚实或希望快速解决时,应优先与员工协商,签订《协商解除协议》。虽然可能需支付略高于法定标准(N+1至2N)的补偿,但能彻底避免劳动争议,成本可控。
|
||||||
|
- **必须规避法律明确保护的员工群体**:辞退处于孕期、产期、哺乳期、医疗期、工伤期间的员工,或在本单位连续工作满15年且距退休不足5年的员工,属于绝对红线,将构成违法解除。
|
||||||
|
- **经济补偿计算必须精确合规**:补偿基数(离职前12个月平均工资)和年限(每满一年支付一个月)的计算错误会引发额外争议。高薪员工(工资超当地社平工资3倍)的补偿年限上限为12年。
|
||||||
|
- **制度是“过失性辞退”的基础**:以“严重违反规章制度”为由辞退,前提是规章制度本身内容合法、经过民主程序制定并已向员工公示。模糊或无效的制度无法作为有效依据。
|
||||||
|
|
||||||
|
## 2026-03-06 — 我想辞退一个员工
|
||||||
|
|
||||||
|
* **合法辞退的核心是“理由法定、证据确凿、程序完备”**。任何单方解除都必须严格对应《劳动合同法》第三十九条(过失)或第四十条(无过失)的法定情形,并由用人单位承担全部举证责任。
|
||||||
|
* **“协商一致解除”是风险最低、成本可控的首选路径**。即使支付略高于法定补偿金(N)的金额,也能彻底避免劳动争议和双倍赔偿金(2N)的高风险。
|
||||||
|
* **必须规避法律保护的“红线”员工群体**。辞退处于孕期、产期、哺乳期、医疗期或工伤期的员工,将直接构成违法解除。
|
||||||
|
* **经济补偿/赔偿金的计算必须精确,尤其注意历史工龄分段**。对于2008年1月1日前入职的员工,补偿金计算需适用当时的法规,规则复杂,易出错。
|
||||||
|
* **内部合规流程(法务审核、通知工会)和规范离职手续(结清款项、出具证明)是避免后续争议的关键程序**。任何程序缺失都可能导致整个辞退行为被认定为违法。
|
||||||
|
|||||||
@ -20,6 +20,7 @@ type Config struct {
|
|||||||
BaseURL string `yaml:"base_url"`
|
BaseURL string `yaml:"base_url"`
|
||||||
APIKeyEnv string `yaml:"api_key_env"`
|
APIKeyEnv string `yaml:"api_key_env"`
|
||||||
Skills []string `yaml:"skills"`
|
Skills []string `yaml:"skills"`
|
||||||
|
CanChallenge bool `yaml:"can_challenge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sdaduanbilei/agent-team/internal/agent"
|
"github.com/sdaduanbilei/agent-team/internal/agent"
|
||||||
@ -67,7 +68,7 @@ type Event struct {
|
|||||||
Type EventType `json:"type"`
|
Type EventType `json:"type"`
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
Agent string `json:"agent,omitempty"`
|
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"`
|
Content string `json:"content,omitempty"`
|
||||||
Streaming bool `json:"streaming,omitempty"`
|
Streaming bool `json:"streaming,omitempty"`
|
||||||
From string `json:"from,omitempty"`
|
From string `json:"from,omitempty"`
|
||||||
@ -80,6 +81,38 @@ type Event struct {
|
|||||||
Filename string `json:"filename,omitempty"`
|
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("<team_board>\n")
|
||||||
|
for _, e := range b.entries {
|
||||||
|
fmt.Fprintf(&sb, " <entry type=\"%s\" author=\"%s\">\n%s\n </entry>\n", e.Type, e.Author, e.Content)
|
||||||
|
}
|
||||||
|
sb.WriteString("</team_board>")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) {
|
func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) {
|
||||||
data, err := os.ReadFile(filepath.Join(roomDir, "room.md"))
|
data, err := os.ReadFile(filepath.Join(roomDir, "room.md"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -138,6 +171,107 @@ func (r *Room) AppendHistory(role, agentName, content string) {
|
|||||||
f.WriteString(line)
|
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:<your concern>. 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.
|
// Handle processes a user message through master orchestration.
|
||||||
func (r *Room) Handle(ctx context.Context, userMsg string) error {
|
func (r *Room) Handle(ctx context.Context, userMsg string) error {
|
||||||
return r.HandleUserMessage(ctx, "user", userMsg)
|
return r.HandleUserMessage(ctx, "user", userMsg)
|
||||||
@ -154,13 +288,13 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string)
|
|||||||
// Build master context
|
// Build master context
|
||||||
teamXML := r.buildTeamXML()
|
teamXML := r.buildTeamXML()
|
||||||
skillXML := skill.ToXML(r.skillMeta)
|
skillXML := skill.ToXML(r.skillMeta)
|
||||||
|
|
||||||
// Build user info XML
|
// Build user info XML
|
||||||
var userXML string
|
var userXML string
|
||||||
if r.User != nil {
|
if r.User != nil {
|
||||||
userXML = r.User.BuildUserXML()
|
userXML = r.User.BuildUserXML()
|
||||||
}
|
}
|
||||||
|
|
||||||
extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillXML
|
extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillXML
|
||||||
systemPrompt := r.master.BuildSystemPrompt(extraContext)
|
systemPrompt := r.master.BuildSystemPrompt(extraContext)
|
||||||
|
|
||||||
@ -190,45 +324,26 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute assignments
|
// Execute assignments in parallel
|
||||||
var results strings.Builder
|
board := &SharedBoard{}
|
||||||
for memberName, task := range assignments {
|
results := r.runMembersParallel(ctx, assignments, board, skillXML)
|
||||||
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})
|
|
||||||
|
|
||||||
memberSystem := member.BuildSystemPrompt(skillXML)
|
// Run challenge round
|
||||||
memberMsgs := []llm.Message{
|
r.runChallengeRound(ctx, board, skillXML)
|
||||||
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})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feed results back to master for review
|
// Feed results back to master for review
|
||||||
r.setStatus(StatusThinking, "", "")
|
r.setStatus(StatusThinking, "", "")
|
||||||
masterMsgs = append(masterMsgs, llm.NewMsg("user", "Team results:\n"+results.String()+"\nPlease review. If satisfied output DONE:<summary>, 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:<summary>, otherwise output ASSIGN instructions for revisions."
|
||||||
|
masterMsgs = append(masterMsgs, llm.NewMsg("user", feedbackMsg))
|
||||||
|
|
||||||
// Update tasks
|
// Update tasks
|
||||||
r.updateTasks(masterMsgs)
|
r.updateTasks(masterMsgs)
|
||||||
|
|||||||
@ -140,9 +140,9 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
if (ev.type === 'agent_message') {
|
if (ev.type === 'agent_message') {
|
||||||
set(s => {
|
set(s => {
|
||||||
const msgs = [...(s.messages[roomId] || [])]
|
const msgs = [...(s.messages[roomId] || [])]
|
||||||
const last = msgs[msgs.length - 1]
|
const idx = msgs.findLastIndex(m => m.streaming && m.agent === ev.agent)
|
||||||
if (last?.streaming && last.agent === ev.agent) {
|
if (idx !== -1) {
|
||||||
msgs[msgs.length - 1] = { ...last, content: last.content + ev.content, streaming: ev.streaming }
|
msgs[idx] = { ...msgs[idx], content: msgs[idx].content + ev.content, streaming: ev.streaming }
|
||||||
} else {
|
} else {
|
||||||
msgs.push({ id: Date.now().toString(), agent: ev.agent, role: ev.role, content: ev.content, streaming: ev.streaming })
|
msgs.push({ id: Date.now().toString(), agent: ev.agent, role: ev.role, content: ev.content, streaming: ev.streaming })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export interface Room {
|
|||||||
export interface Message {
|
export interface Message {
|
||||||
id: string
|
id: string
|
||||||
agent: string
|
agent: string
|
||||||
role: 'user' | 'master' | 'member'
|
role: 'user' | 'master' | 'member' | 'challenge'
|
||||||
content: string
|
content: string
|
||||||
streaming?: boolean
|
streaming?: boolean
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ export interface SkillMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type WsEvent =
|
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: 'room_status'; status: RoomStatus; active_agent?: string; action?: string }
|
||||||
| { type: 'task_assign'; from: string; to: string; task: string }
|
| { type: 'task_assign'; from: string; to: string; task: string }
|
||||||
| { type: 'tasks_update'; content: string }
|
| { type: 'tasks_update'; content: string }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user