Skip to content
Merged
120 changes: 120 additions & 0 deletions docs/changelog.md

Large diffs are not rendered by default.

142 changes: 142 additions & 0 deletions docs/group-conversations.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,148 @@ Both caps are enforced independently: `maxPerTurn` bounds a runaway single turn,
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.

## Voting

A `VOTE` phase collects **explicit ballots** instead of another round of prose.
LLM ballots are correlated (shared priors, sycophancy) — the durable value is
the auditable artifact: the weighted tally, every raw ballot, and the losing
side's statements recorded as dissents on the `DecisionRecord`.

```json
{
"name": "Ballot", "type": "VOTE",
"turnOrder": "PARALLEL", "contextScope": "NONE",
"voteConfig": {
"method": "MAJORITY",
"optionsSource": "EXPLICIT",
"options": ["Adopt PostgreSQL", "Stay on MongoDB"],
"quorum": 0.5,
"weights": { "senior-architect": 2.0 },
"weightByConfidence": false,
"tiePolicy": "MODERATOR_DECIDES"
}
}
```

**Independence is enforced structurally, not advised.** Save-time validation
rejects a VOTE phase that is not `PARALLEL` + `contextScope: NONE`; ballots are
cast blind against the pre-fan-out snapshot, and `VOTE` entries stay
peer-hidden until their phase completes (commit-reveal).

- **Ballot contract:** `{"vote": "<exact option text>", "confidence": <0..1>,
"statement": "..."}` (`APPROVAL` uses `"votes": [...]`). Three-tier parse:
strict JSON → JSON embedded in prose → a reply naming exactly one option's
text. Anything else is a non-ballot and **counts against quorum** — as do
abstentions; a mostly-silent team has not reached quorum, and that is signal.
- **Options:** `EXPLICIT` is the reliable path. `LAST_SYNTHESIS` extracts
`Option A: …` lines from the newest synthesis — instruct that synthesis to
emit them.
- **Ties and quorum failures** go to `tiePolicy`: `MODERATOR_DECIDES` runs one
moderator turn choosing among the unresolved options (method
`vote+moderator-tiebreak`); `NO_DECISION` (default) records an honest
`type: NONE` and the discussion continues. `HUMAN_DECIDES` is reserved for
human group members (I6) and is rejected at save time until then.
Comment on lines +199 to +203

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the HUMAN_DECIDES status now that I6 ships in the same change.

Lines 202-203 state that HUMAN_DECIDES "is reserved for human group members (I6) and is rejected at save time until then". The next section documents human group members as available. AgentGroupStore.validateVotePhases still rejects HUMAN_DECIDES with the message "needs human group members (I6), which are not available yet". A reader who just enabled a HUMAN member will expect HUMAN_DECIDES to work. State explicitly that HUMAN_DECIDES remains unimplemented in this release even with human members configured.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/group-conversations.md` around lines 199 - 203, Clarify the
`HUMAN_DECIDES` description in the tie-policy documentation to state that it
remains unimplemented and is rejected at save time in this release, even when
human group members are configured. Keep the existing references to human
members and the `I6` context while removing the implication that availability
alone enables the policy.

- The result fires the `decision_reached` SSE event (which this feature also
wires for debate verdicts) and renders a tally block in Slack.

## Humans as group members (I6)

Real deployments are hybrid teams: a `memberType: "HUMAN"` member sits in the
roster like any agent, but their turn **pauses the discussion**
(`AWAITING_HUMAN_INPUT`) until they answer.

```json
{
"members": [
{ "agentId": "agent-1", "displayName": "Analyst", "speakingOrder": 1 },
{ "agentId": "gregor@example.com", "displayName": "Gregor", "speakingOrder": 2, "memberType": "HUMAN" }
],
"humanMemberConfig": { "turnTimeout": "PT4H", "onTimeout": "SKIP_TURN" }
}
```

- The human's `agentId` is their **principal id** — the identity that may submit
their turns; `displayName` is required at save time.
- Their prompt is rendered exactly like an agent's and persisted on the
conversation (`pendingHumanInput.renderedPrompt`); the `human_input_requested`
SSE event (and a Slack notice) says who is up.
- Submission: `POST /groups/{groupId}/conversations/{id}/human-input`
`{memberId, content}` or MCP `submit_group_human_input`. **Only the member's
own principal (or an admin) may submit** — an `eddi-approver` may decide
approvals, but speaking as another human is impersonation, not review. The
answer is recorded as the phase's natural entry type (a human OPINION is an
OPINION) and the discussion resumes from the next speaker.
- This is deliberately NOT the approval surface: approve/reject endpoints never
accept free text, and the pending-approvals inbox marks these entries
`pauseType: "HUMAN_TURN"` with the member's id, so a human sees their own
pending turns without owning the conversation.
- **Timeouts** (`humanMemberConfig`): `turnTimeout` (ISO-8601; unset = wait
indefinitely) with `onTimeout: SKIP_TURN` (a SKIPPED entry — "no response from
<name> within <window>" — and the discussion moves on) or `ABORT` (graceful
cancel). Timeout schedules survive restarts via the HITL crash-recovery sweep.
- **PARALLEL phases**: agents fan out first; humans are then prompted one at a
time against the *pre-fan-out* snapshot, so an independent round stays
independent — a human answering after the agents cannot read their answers.
- **v1 bounds (save-time rejected)**: no HUMAN members in task-force groups
(PLAN/EXECUTE/VERIFY) or `targetEachPeer` phases, and a group containing
humans cannot be nested as a GROUP member. A human **moderator** is allowed —
every synthesis then waits on that person (the save warns about it).

## Facilitator with bounded moves (I12)

Orchestration is static choreography; nothing reacts ("we've agreed — stop",
"we need a specialist — recruit"). A full LLM orchestrator conflicts with
deterministic governance, so the facilitator chooses among
**config-enumerated moves** — validated, capped, audit-logged:

```json
{
"facilitator": {
"enabled": true,
"agentId": "facilitator-agent",
"allowedMoves": ["CONTINUE", "CALL_VOTE", "ESCALATE_HUMAN"],
"checkAfter": "EACH_PHASE",
"maxMovesPerDiscussion": 10,
"escalateTo": "gregor@example.com"
}
}
```

At each checkpoint (`EACH_PHASE` default, or `EACH_REPEAT`) the facilitator
agent receives a **compact briefing** — phase/repeat position, budget
arithmetic, roster, entry counts, capped excerpts, *never the full transcript*
— and replies `{"move", "args", "reason"}`. Moves v1:

- `CONTINUE` — the ambient no-op and every failure's fallback. Silent.
- `END_PHASE` / `EXTEND_PHASE` — skip the remaining repeats / add one (≤2
extensions per phase, still bounded by `maxTurns`). They act mid-phase, so
the save **rejects** them unless `checkAfter: EACH_REPEAT`. A convergence
exit is never overruled.
- `CALL_VOTE` — inserts a one-off VOTE phase next (`args.options`, 2–10),
built to I14's enforced PARALLEL+NONE shape by construction.
- `RECRUIT` — brings a deployed agent in (`args.agentId`), the same
validation path as I7's `recruitAgent` tool.
- `ESCALATE_HUMAN` — pauses the discussion (`AWAITING_HUMAN_INPUT`) for the
configured `escalateTo` principal with the facilitator's question, riding
I6's pending-input machinery whole; the answer records as a peer-visible
`FOLLOW_UP` and the discussion resumes.

**Bounds and honesty.** The consult itself is a real LLM turn (counted, cost
on the I1 ledger, skipped once either budget is gone). Executed non-CONTINUE
moves are capped by `maxMovesPerDiscussion` (default 10) and each lands as a
peer-hidden `FACILITATION` entry + audit event + the
`eddi_group_facilitator_moves_total{move,outcome}` counter. Anything
unparseable, disallowed or invalid-in-context degrades to CONTINUE **and is
recorded as a rejected attempt** — the audit trail must show the model tried.
Facilitator failure never fails a discussion.

**Runtime phase divergence.** CALL_VOTE and EXTEND_PHASE mutate a runtime
copy of the phase list, persisted on the conversation (schema v4). Every
resume path — approval, human turn, timeout skip — executes and drift-checks
against that list, so a pause taken inside an inserted phase resumes
correctly. The divergence is one-off: completion (and every new round) starts
from the config again.

## 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
Loading
Loading