Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
> **Purpose:** Living document tracking all changes, decisions, and reasoning during implementation. Updated as work progresses for easy reference and review.


---

## 🔎 fix(groups): I18 PR #642 review round 1 (2026-08-08)

**Repo:** EDDI (`feat/group-i18-bidding`)

All 6 CodeQL log-injection findings in `TaskForceEngine`'s bid round accepted and fixed: `LogSanitizer` applied to every caller-influenced value in the bid-round logs (groupId, task subject, bidder/winner agentId, exception messages) — 7 sites sanitized, the 6 flagged plus the bid-turn-failure log the scan will otherwise flag next round. Bid suites (51) green.

---

## 🏷️ feat(groups): I18 — bid-based task assignment, CNP-lite (2026-08-08)

**Repo:** EDDI (`feat/group-i18-bidding`)

Second Wave 3 queue item (adopted from the research review, scoped down — the turn-auction extension stays REJECTED: an extra call per member per turn to decide who talks doubles cost to save cost). The planner cannot know members' actual fit or load; the Contract Net Protocol's announce-bid-award loop maps onto the existing wave scheduler.

- **`assignmentMode = ROLE (default) | BID`** on `TaskDefinition` (per task) and `GroupTaskConfig` (group default), both with compat constructors — every pre-I18 config resolves to ROLE through `TaskBidEngine.effectiveMode`'s task → group → ROLE chain.
- **PLAN leaves BID-mode tasks unassigned** (both the pre-configured and LLM-planned paths) — assigning there would preempt the auction with the planner's guess.
- **The wave's bid round** (`TaskForceEngine.runBidRoundIfNeeded`, before each wave's grouping so awards join the same wave): eligible members (non-moderator AGENTs) each get one **blind, parallel** bid turn — the prompt carries the announced batch and NOTHING else (no transcript, no peer bids; blindness is what makes the self-assessed confidences comparable, and the honesty rule is stated to the model: an inflated confidence wins you work you will fail at, on the record). Replies land as `BID` transcript entries — F4's blind-bid visibility (peer-hidden while the phase runs) has its first producer.
- **Deterministic award, never a stalled wave**: highest confidence per task; ties break by speaking order then agent id (identical on every pod); a task nobody bid on falls back to ROLE/round-robin; the auction skips itself with a LOG (a silent cap reads as coverage) when <2 bidders, <2 unassigned tasks, or the turn budget cannot cover one bid turn per member. Bid turns count toward the turn budget and their cost flows through the normal member-turn attribution.
- **The award is per-task metadata** (`SharedTaskList.awardedBids[taskId] = AwardedBid{agentId, confidence, estimatedComplexity, rationale}`), deliberately NOT a global DecisionRecord — an award is a scheduling fact about one task, not the discussion's conclusion.
- Parse discipline mirrors `VoteTallyEngine`: three tiers, FAIL_ON_TRAILING_TOKENS, unknown subjects dropped (an out-of-contract bid is never guessed onto a task), confidence clamped to [0,1], first-bid-per-task within one reply.

**Tests (13 new: 8 `TaskBidEngineTest` + 5 `TaskForceEngineTest`; `engine.internal` + `configs.groups` suites 1694 green; checkstyle clean):** parse tiers + clamping + unknown-subject drop; award to highest confidence; tie-break determinism (speaking order, then agent id); no-bids absence; effective-mode chain; worthwhile-auction caps; blind prompt content; engine-level award with recorded bid + turn accounting + BID entries; **blindness asserted on the captured prompts** (no peer rationale/confidence leaks); no-bids ROLE fallback never stalls; skip-conditions make zero LLM calls; ROLE-mode tasks never auctioned.

---

## 🔀 merge: bring `origin/main` (PR #627 HITL request pinning) into the branch (2026-08-07)
Expand Down
33 changes: 33 additions & 0 deletions docs/group-conversations.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,44 @@ tools.
a second writer racing it would corrupt the state machine that decides what runs
next. Filing is the only agent-side write.


Both caps are enforced independently: `maxPerTurn` bounds a runaway single turn,
`maxAgentAddedTasksPerDiscussion` bounds slow drift across a long discussion. The
discussion cap counts only agent-filed tasks, so a large planned backlog does not
exhaust it. A rejected call does not consume the per-turn budget.

### Bid-based assignment (I18, CNP-lite)

The planner cannot know members' actual fit or load. Setting
`assignmentMode: "BID"` — per task on a `TaskDefinition`, or as the group
default on `taskListConfig` — leaves those tasks unassigned at PLAN time; the
execution wave **announces** them to eligible members in blind, parallel bid
turns and **awards** each to the highest self-assessed confidence:

```json
"taskListConfig": { "assignmentMode": "BID" },
"tasks": [
{ "subject": "Write the migration", "description": "...", "assignmentMode": "BID" }
]
```

- Members reply `{"bids": [{"subject", "confidence": 0..1, "estimatedComplexity":
"XS|S|M|L", "rationale"}]}` — three-tier parse; prose casts no bids; bids on
unannounced tasks are dropped; confidence is clamped.
- **Blind**: the bid prompt carries the announced tasks and nothing else — no
transcript, no peer bids. Bid replies land as `BID` transcript entries
(peer-hidden while the phase runs, auditable afterwards).
- **Deterministic award**: highest confidence; ties break by speaking order,
then agent id. The winning bid is recorded per task
(`taskList.awardedBids[taskId] = {agentId, confidence, estimatedComplexity,
rationale}`).
- **Never stalls a wave**: a task nobody bid on falls back to the ROLE path,
and the auction skips itself entirely (logged) when it cannot beat its own
overhead — fewer than 2 eligible bidders or fewer than 2 unassigned tasks —
or when the remaining turn budget cannot cover one bid turn per member.
- Bid turns are real member turns: they count toward the turn budget and their
cost lands in the discussion's cost attribution.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
## Nested Groups (Group-of-Groups)

Members can be other groups. The sub-group runs its own discussion and its synthesized answer becomes the member's response.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ public void setTaskListConfig(GroupTaskConfig taskListConfig) {
* files twenty tasks in one breath, where a discussion-long drift
* files one per turn for forty turns
*/
public record GroupTaskConfig(boolean allowAgentTaskCreation, int maxAgentAddedTasksPerDiscussion, int maxPerTurn) {
public record GroupTaskConfig(boolean allowAgentTaskCreation, int maxAgentAddedTasksPerDiscussion, int maxPerTurn,
AssignmentMode assignmentMode) {

public static final int DEFAULT_MAX_PER_DISCUSSION = 20;
public static final int DEFAULT_MAX_PER_TURN = 3;
Expand All @@ -136,6 +137,14 @@ public record GroupTaskConfig(boolean allowAgentTaskCreation, int maxAgentAddedT
if (maxPerTurn <= 0) {
maxPerTurn = DEFAULT_MAX_PER_TURN;
}
if (assignmentMode == null) {
assignmentMode = AssignmentMode.ROLE;
}
}

/** Backward-compatible constructor without {@code assignmentMode} (I18). */
public GroupTaskConfig(boolean allowAgentTaskCreation, int maxAgentAddedTasksPerDiscussion, int maxPerTurn) {
this(allowAgentTaskCreation, maxAgentAddedTasksPerDiscussion, maxPerTurn, AssignmentMode.ROLE);
}

/** Disabled, with both caps at their defaults. */
Expand Down Expand Up @@ -566,12 +575,18 @@ public record TaskDefinition(
String description,
String assignToRole,
List<String> dependsOn,
int priority) {
int priority,
AssignmentMode assignmentMode) {

public TaskDefinition(String subject, String description) {
this(subject, description, "ALL", List.of(), 0);
}

/** Backward-compatible constructor without {@code assignmentMode} (I18). */
public TaskDefinition(String subject, String description, String assignToRole, List<String> dependsOn, int priority) {
this(subject, description, assignToRole, dependsOn, priority, null);
}

public TaskDefinition {
Objects.requireNonNull(subject, "Task subject must not be null");
Objects.requireNonNull(description, "Task description must not be null");
Expand All @@ -584,6 +599,22 @@ public TaskDefinition(String subject, String description) {
}
}

/**
* How a task finds its agent (I18, CNP-lite). {@code null} on a task falls back
* to {@code taskListConfig.assignmentMode()}, and to {@link #ROLE} when that
* too is unset — every config that predates I18 behaves exactly as before.
*/
public enum AssignmentMode {
/** The planner/config assigns by role or round-robin — today's behavior. */
ROLE,
/**
* Contract-Net-lite: eligible members submit blind parallel bids and the
* highest confidence wins (deterministic tie-break by speaking order; no bids
* falls back to ROLE — a wave never stalls on an auction).
*/
BID
}

// --- Dynamic Agent Configuration ---

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
* Runtime task list for task-oriented group conversations. Embedded in
Expand Down Expand Up @@ -607,4 +609,41 @@ public synchronized List<TaskItem> getTasks() {
public synchronized void setTasks(List<TaskItem> tasks) {
this.tasks = tasks != null ? new ArrayList<>(tasks) : new ArrayList<>();
}

// ==================== I18 — bid awards (CNP-lite) ====================

/**
* The winning bid a BID-mode task was awarded on (I18) — per-task metadata,
* deliberately not a global {@code DecisionRecord}: an award is a scheduling
* fact about one task, not the discussion's conclusion.
*
* @param agentId
* the winner
* @param confidence
* the winning self-assessed confidence (0..1, clamped at parse)
* @param estimatedComplexity
* the bidder's XS/S/M/L estimate, verbatim
* @param rationale
* why the bidder claimed the task — quoted by dashboards
*/
public record AwardedBid(String agentId, double confidence, String estimatedComplexity, String rationale) {
}

/** taskId → the bid it was awarded on. Only BID-mode tasks ever appear here. */
private Map<String, AwardedBid> awardedBids = new ConcurrentHashMap<>();

public Map<String, AwardedBid> getAwardedBids() {
return awardedBids;
}

public void setAwardedBids(Map<String, AwardedBid> awardedBids) {
this.awardedBids = awardedBids != null ? new ConcurrentHashMap<>(awardedBids) : new ConcurrentHashMap<>();
}

/** Records the award a task's assignment came from (I18). */
public synchronized void recordAwardedBid(String taskId, AwardedBid bid) {
if (taskId != null && bid != null) {
awardedBids.put(taskId, bid);
}
}
}
Loading
Loading