For the complete documentation index, see llms.txt. This page is also available as Markdown.

Group Conversations

Multi-agent structured discussions with moderator synthesis.

Overview

Group Conversations enable multiple agents to discuss a question. Each agent participates through its normal pipeline — agents are group-unaware by default. A GroupConversationService orchestrates the discussion through configurable phases.

Discussion Styles

Style
Flow
Best For

ROUND_TABLE

Opinion × N → Synthesis

Brainstorming, open-ended exploration

PEER_REVIEW

Opinion → Critique → Revision → Synthesis

Code review, document review

DEVIL_ADVOCATE

Opinion → Challenge → Defense → Synthesis

Risk assessment, stress-testing

DELPHI

Anonymous rounds → convergence → Synthesis

Forecasting, reducing groupthink

DEBATE

Pro → Con → Rebuttals → Judge

Trade-off analysis, comparisons

TASK_FORCE

Plan → Execute → Verify → Synthesis

Structured task decomposition, parallel execution

NEGOTIATION

Positions → Proposals → Bargaining → (Arbitration) → Synthesis

Surfacing trade-offs, drafting compromises

CUSTOM

Define your own phases

Any workflow

Quick Start (MCP)

# 1. Discover available styles
describe_discussion_styles

# 2. Create a group
create_group(
  name="Architecture Review",
  memberAgentIds="expert-1,expert-2,expert-3",
  memberDisplayNames="Backend Expert,Frontend Expert,DevOps Expert",
  moderatorAgentId="moderator-agent",
  style="PEER_REVIEW"
)

# 3. Run a discussion
discuss_with_group(groupId="<id>", question="Should we use microservices?")

Quick Start (REST)

One agent, one seat. A member is addressed everywhere by its agentId — its member conversation, display name, vote weight, participants lists and the facilitator all key on it — so listing the same agent twice does not give the group two panelists: both seats would speak through one member conversation, and the "independent" second answer would read the first as its own history. A group config that repeats a member (same agentId, whatever the member type — the runtime keys every seat by the id alone) is rejected, naming both positions: REST POST/PUT /groupstore/groups answer 400, and the MCP tools create_group, update_group and create_group_from_template return the same message as a JSON error payload ({"error": "<tool> failed: members[1] repeats members[0] …"}) — MCP tool calls have no HTTP status of their own. For a second seat with the same behaviour, duplicate the agent and add the copy. A group already stored with a repeated member still loads: each repeat is dropped on read — the first seat wins, with its display name, role and speaking order — and a warning is logged; saving the group once stores the cleaned list. One case needs a roster change first: if the dropped seat held a role a preset style is built on — typically one agent seated as both PRO and CON in a DEBATE — the save is refused with the ordinary role message naming the missing role (that group never had two sides; one conversation argued both). The warning logged on read names the dropped role; put a second agent in that seat and save.

Member Roles

Some styles require specific roles. A preset DEBATE group without at least one PRO and one CON member, or a preset DEVIL_ADVOCATE group without a DEVIL_ADVOCATE member, is rejected at save time — the engine would otherwise fall back to ALL members and the style would silently become something else. (Groups with explicit phases route roles themselves and are not checked.)

Role
Used By
Purpose

DEVIL_ADVOCATE

DEVIL_ADVOCATE style

Argues against consensus

PRO

DEBATE style

Argues in favor

CON

DEBATE style

Argues against

Debate verdicts

When a DEBATE has two sides and an impartial judge — i.e. members carry at least two distinct roles and moderatorAgentId names an agent that is not one of the debaters — the judgment phase returns a structured verdict alongside its prose, on the conversation's decision field:

winner is null for a tie (the tie is stated in outcome). The judge is asked to score argument quality and factual support, and explicitly not assertiveness or fluency.

Any of these leaves decision unset and the conclusion as ordinary prose — none of them is an error:

  • No roles on the members. Nothing argued PRO, so a PRO-vs-CON score would be invented rather than measured.

  • No moderator. The stand-in synthesizer is one of the debaters, and a partisan's call is not the group's finding.

  • A phase inputTemplate of your own. Your instruction wins; set one if you want a debate to conclude in plain prose.

  • A judgment the parser cannot read. The prose conclusion is kept as-is.

Agent-filed tasks

Work an agent discovers mid-discussion — a missing migration, an untested edge case — would otherwise die in prose: the shared task list is written only by the PLAN phase and by config. Turning on taskListConfig gives members two tools, addGroupTask and listGroupTasks, and a filed task is picked up by the next execution wave with no other changes.

Off by default, and absent rather than refusing when off. A tool that is not assembled costs no prompt tokens and cannot be argued with; one that exists and always says no invites retries. The tools appear only when the turn belongs to a live group discussion whose config sets allowAgentTaskCreation: true — a standalone agent, a paused discussion, or an unreadable config all yield no tools.

addGroupTask(subject, description, dependsOnSubjects?, priority?, assignToRole?)

  • dependsOnSubjects names other tasks by their subject, as listGroupTasks prints them. An unknown name is refused, not dropped — silently filing a task without its dependency schedules it immediately, which is the opposite of what was asked.

  • assignToRole takes "ROLE:Reviewer" or a member's exact name, and files the task already assigned. Omitting it (or passing "ALL") round-robins the task to the next member through the same resolver the PLAN phase uses — every filed task gets an owner, and filing is refused outright if the team has nobody to own it. An unmatched role is refused with the available roles named.

  • Refusals are sentences aimed at the model: duplicate subject, unknown dependency, circular dependency, subject over 200 chars, description over 4,000, and either cap being reached.

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.

No claim or complete tools. The wave loop owns every task-state transition; a second writer racing it would corrupt the state machine that decides what runs next. Filing is the only agent-side write.

Transcript windowing

A FULL- or ANONYMOUS-scope discussion phase re-feeds the transcript to every member every turn, so a long discussion grows roughly quadratically in prompt tokens. contextWindow bounds what each turn renders — the stored transcript is never modified, and signing verification still runs on the raw entries:

When the scope-filtered context exceeds maxRecentEntries, the older entries collapse into a single leading block: a rolling summary (extended incrementally at phase boundaries via the shared summarization service — never per member turn), or, with summarizeOverflow: false, a plain [n earlier entries omitted] marker that costs no LLM call. A summarization failure never blocks the discussion — the turn renders with the truncation marker and the next boundary catches up.

  • Applies to FULL and ANONYMOUS scopes only. The ANONYMOUS variant keeps its own summary, built from the same "Anonymous" labels the scope filter produces — the summary can never de-anonymize a peer.

  • llmProvider/llmModel name the summarizer; without them, overflow falls back to the truncation marker (a save-time warning says so).

  • The optional prices attribute the summarizer's own spend to the discussion's cost ledger, where maxCostPerDiscussion sees it.

  • The convergence judge's input is not windowed — it is already bounded to the last two rounds.

Member stances (the overview dashboard)

The Manager and the Workforce board can render a discussion as an overview instead of a transcript — a phase rail, a members x phases matrix, and a roster of one-line positions. The roster's lines are member stances, and they always exist: by default each is the lead sentence of that member's newest contribution, which costs nothing and is the member's own words.

stanceSummary upgrades that to an LLM summary of everything the member has said:

  • The summarizer's input is bounded. It reads the member's newest contributions within maxInputChars (default 8,000, ceiling 32,000), each cut to maxEntryChars (default 2,000, ceiling 8,000), with an [N earlier contribution(s) omitted] marker — a stance is where the member stands now, and the whole history re-sent at every boundary eventually overflows the summarizer's context window. Non-positive values take the default.

  • There is no enabled flag, unlike contextWindow. Stances exist either way, so the only thing a flag could have meant is "may this spend money?" — which is already what naming a provider and a model means.

  • Stances are recomputed at phase boundaries, and only for a member whose stored stance no longer covers their own contributions. A member who stayed silent through a phase costs nothing — coverage is counted per member, not against the transcript length, or one member speaking would re-bill everyone.

  • The summarizer is optional spend and obeys maxCostPerDiscussion, like the window summarizer and the convergence judge. Past the ceiling it downgrades to extraction rather than stopping the phase.

  • A summarizer failure degrades to lead-sentence extraction, never to a blank roster, and never fails the discussion.

  • The UI distinguishes the two producers — an extracted line is shown as a quotation, a generated one as a summary — because presenting a paraphrase the way a quotation is presented would misattribute it.

  • Extraction skips the entry types that carry a JSON contract (VOTE, BID, RETRO, PLAN, TASK_RESULT, VERIFICATION) and quotes the newest prose entry instead — the lead "sentence" of a ballot is an opening brace, and it would otherwise replace a member's real position after every vote. The LLM summarizer still reads them.

  • The optional prices attribute stance spend to the same cost ledger maxCostPerDiscussion bounds, under a system:stance:* key.

  • Naming only one of llmProvider/llmModel (or prices with neither) produces a save-time warning: that config intends to spend and silently will not.

Both surfaces stream two events for this view: cost_updated after every cost attribution (carrying the key's cumulative spend, so a redelivered frame is idempotent) and stance_updated whenever a stance's text changes.

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.

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.

    • Only what was cast counts. For a JSON ballot, only the vote/votes values are read, never the free-text statement. A cast in a shape the method did not ask for still counts when it names exactly one option (the approval array {"votes": ["X"]} under MAJORITY, a string "votes": "X" under APPROVAL).

    • An empty ballot is a non-vote. "votes": [], "vote": null or a JSON ballot with no vote field at all counts against quorum — even if its statement mentions an option.

    • Options are matched as words. In prose ballots (and the moderator's tiebreak reply) an option must appear as a whole word or phrase, case-insensitively: option No is not found inside "not" or "know". The boundary applies only between words of scripts written with spaces — in Chinese, Japanese, Thai and similar scripts a plain substring match is used, so "我支持方案A。" is a vote for 方案A.

  • Options: EXPLICIT is the reliable path. LAST_SYNTHESIS extracts Option A: … lines from the newest synthesis — instruct that synthesis to emit them (the default synthesis prompt does not). The line may be written the way models write it: **Option A:** …, - Option B: …, 1. Option C) …, Option D. …, in any letter case.

  • 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 still rejected at save time: humans can now be group members (I6), but a tie-break that waits on a person needs its own resume machinery, which has not shipped.

  • 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.

  • 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 within " — 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:

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.

Negotiation (I11)

EDDI's other decision forms are win/lose; NEGOTIATION is the trade form — a process for surfacing trade-offs whose output is a drafted compromise with an explicit concession ledger for human sign-off.

The preset: ① Positions & Interests (parallel, context-free — interests enable integrative trades) ② Opening Proposals ③ Bargaining (repeats = maxRounds, exits early on agreement) ④ Arbitration (moderator; skipped entirely when an agreement was reached — skipIf: "AGREEMENT_REACHED", the single deterministic skip condition) ⑤ Synthesis.

  • A BARGAIN turn is a typed move: {"accept": "<proposalId>"|null, "proposal": {"terms": "..."}|null, "concessions": [{"gaveUp": "...", "inReturnFor": "..."}]} plus free-text reasoning. Three-tier parse; an unreadable turn is prose with no state effect.

  • The typed structure is the anti-sycophancy mechanism: an acceptance must name a specific proposal id, a concession that names nothing in return is not recorded, and the open proposals + ledger are quoted into every turn — the record the outcome will cite.

  • A new proposal supersedes the mover's own open one (one live offer per agent); the proposer signs their own terms implicitly.

  • Agreement = every non-moderator participant signed the same open proposal. The bargaining phase ends its repeats early and the conversation carries decision: {type: "AGREEMENT", method: "negotiation"} whose tally.signedAcceptances maps each signatory to the transcript index of their signed acceptance entry — the (already signed) entries are the co-signatures; no new crypto.

  • No agreement → the arbitration runs and its conclusion becomes decision: {type: "VERDICT", method: "arbitration"}.

  • The ledger is bounded — it is an LLM write surface quoted into every later turn. One BARGAIN turn records at most maxConcessionsPerMove concessions (default 5, ceiling 20; each side of a concession is stored truncated to 600 characters), the ledger stops at maxLedgerConcessions (default 50, ceiling 500 — the earliest are kept, they are the record the outcome quotes; further concessions are not recorded but counted, and the agreement outcome says "N further concession(s) not recorded (ledger full)" and carries tally.concessionsNotRecorded), and a turn's prompt quotes only the newest maxRenderedConcessions (default 20, ceiling 100) with an "(N earlier concession(s) omitted)" line. negotiationConfig is a default, not a switch: without it the defaults apply.

Retro → group memory

A RETRO phase stops discussions from evaporating: the group reviews how it worked and distills lessons that persist as team-owned group memory, then surface as {properties.*} in every member's later discussions — institutional knowledge that compounds run-over-run.

  • The built-in template asks for {"lessons": [{"lesson": "...", "context": "..."}]}; parsing is three-tier (strict JSON → embedded JSON → nothing) and the per-run cap is enforced at parse time whatever the model produced.

  • Lessons are stored under the synthetic owner group:<groupId> with group visibility — they belong to the team, not to whichever human ran the discussion, and survive that human's GDPR erasure without carrying their identity. The idempotency key retro:<hash(lesson)> makes re-running the same retro a no-op.

  • Bounded growth is non-negotiable: the stored set FIFOs at maxStoredLessons — storing past the cap evicts the oldest, ordered by creation time, so re-harvesting an old lesson cannot shield it from eviction. Both caps are clamped to hard ceilings (20 per run, 500 stored) no config can exceed: an LLM-driven write surface with an operator-supplied ceiling is only as bounded as the operator's typing.

  • maxLessonsPerRun bounds the whole harvest, not each contribution — a RETRO phase with several participants or repeats cannot multiply it.

  • maxLessonChars (default 1000 — the same default as an agent's memoryGuardrails.maxValueLength; ceiling 4000) bounds one stored lesson value, lesson plus its "applies:" context. Longer lessons are truncated, the context first; the idempotency key still hashes the untruncated lesson.

  • Member conversations already load group-visible entries at init, so recall needs no new namespace. The retro_recorded SSE event reports each harvest.

Standing Teams (I13)

A group conversation is an episode; a team persists. The GroupWorkspace (one document per group, own collection — stored under the group's own id, so two pods creating it at once end up with exactly one, see below) holds what survives between episodes: a backlog (a SharedTaskList, so pulled tasks flow straight into the task-force machinery), cadences that pull from it on a schedule, and the team's running metrics — deliberately thin glue over the existing schedulers, I1's cost ledger and I8's retro memory.

cronExpression is a 5-field Unix cron (min hour day month weekday), not 6-field Quartz — "0 0 3 * * ?" is rejected. The error message says so, but the example above is the one to copy.

MCP: add_team_task, list_team_backlog. The backlog caps at 200 with an actionable error — it is a working set, not an archive.

Cadence fires ride the schedule machinery whole (SchedulePollerService claim/lease/retry/dead-letter; the executor branches on teamCadenceType: "team_cadence" exactly like Dream consolidation). Each fire:

  1. Reconcile — a finished previous run is written back first; one still in flight skips the fire. A run paused at an HITL gate is not terminal, so it holds the claim — but only up to a TTL (see Stale claims below).

  2. Pull — top-N executable backlog tasks by priority; an empty pull skips, logged.

  3. Run — the discussion is started first, so the claim can carry its real id. Pulled tasks are injected as a runtime copy of config.tasks (the stored config is never written) and the cadence's maxCostPerRun rides the inherited-ceiling slot: dollar-primary, per the Dream precedent. The discussion runs under the cadence creator's identity.

  4. Claim — a conditional store write on runningDiscussionId, taken before any task turn can complete and without any in-JVM lock. Two pods firing concurrently both start a discussion; the one that loses the CAS cancels its just-started discussion before its first turn, so only one fire does work.

Writeback happens at the next fire (or on a workspace read — read-repair), never from inside the discussion thread, so a pod crash mid-discussion loses nothing. It also means a fire's COMPLETED status says the discussion was started, not that its tasks are done: backlog statuses change only once that writeback has run. VERIFIED outcomes stay VERIFIED on the backlog and credit the assignee's perMemberStats; anything else returns to PENDING with the reviewer's feedback appended to the description — the cross-run retry loop. A FAILED, REJECTED or CANCELLED discussion returns every pulled task untouched. Retro lessons flow through I8 unchanged (no duplication).

Stale claims are reclaimed. The default group HITL timeout policy is WAIT_INDEFINITELY, so a pause nobody resolves never reaches a terminal state: without a backstop the claim would be held forever, every subsequent fire skipped as "still running", and the pulled backlog tasks stuck IN_PROGRESS. A claim older than eddi.groups.cadence.claim-ttl (default PT24H) is reclaimed — the stranded discussion is cancelled and put through the ordinary failure writeback, so pulled tasks return to PENDING and the claim clears. Cancel happens before release, so a reclaimed run cannot keep spending against a budget nobody is tracking. Set the TTL non-positive to disable reclaiming, for an operator who would rather wedge than risk abandoning a pause; the counter is eddi_team_cadence_claims_reclaimed_total. Creating a cadence on a group that combines requiresApproval phases with WAIT_INDEFINITELY logs a warning — that combination is what makes the backstop reachable — but is not rejected: it is legitimate for a team whose approver really is always available.

Creation is atomic. The workspace is created on first use, and that first use can happen on several pods at once (two backlog adds, a cadence fire and a read). It is written with an insert-only write under the group's id, so the database's primary key admits one creator and every other caller adopts that document. Up to 6.5.0 each creator inserted its own document and they converged afterwards on the lexicographically smallest id — which is not the first insert (ObjectIds are minted client-side, UUIDs are random), so a caller could get back a document that later reads did not elect, and a backlog task it wrote there reported success and vanished. Workspaces created by earlier releases keep their generated id and are still found by their groupId.

Per-member stats are reliability recording only — nothing routes or weights on them in v1. A permanent group deletion cascades to the workspace; a soft (versioned) delete keeps it, because the group can come back. Not in v1: cross-team handoff, Manager UI, reputation weighting.

Shared artifacts (blackboard-lite)

Without artifacts, the transcript is the only medium — every structured thing an agent produces is prose the next agent re-parses. artifactConfig gives members four tools to create together: createArtifact(name, type, content), readArtifact(nameOrId), proposeArtifactUpdate(nameOrId, content, expectedVersion, markFinal?) and listArtifacts(). Artifacts are typed documents (TEXT, MARKDOWN, JSON) in their own collection, listed on the discussion's REST/MCP status payload as artifacts, and announced over SSE and Slack as artifact_updated events.

Concurrency is deterministic compare-and-set, not an LLM merge. Every update presents the version it read; a stale writer is told "artifact changed since you read it (now v3); re-read and merge your change" and retries against fresh content. The failure mode is a retry, never a silent bad merge.

Validators are declarative only — JSON_SCHEMA, REGEX (content must contain a match), MAX_LENGTH (characters) — never code. Specs are checked at config save time; at write time a failing validator refuses the write with its message and stores nothing. Content is additionally capped at 256 KB per artifact.

Off by default with the same absence discipline as the task tools: no opt-in means the tools are never assembled. The member agent's own enableBuiltInTools switch still applies — a member whose LLM task does not set it takes part without the tools, and saving a group that enables artifacts, agent task creation or dynamic agents logs that prerequisite. Artifact, task and dynamic-agent tool results are never served from the tool cache: every member runs as the same user, so a cached listArtifacts() would hide a peer's new artifact. markFinal: true freezes an artifact — FINAL artifacts accept no further updates. Artifacts are deleted with their discussion (close/delete cascade) and by GDPR erasure; the durable trace of the work is the transcript.

Preset Templates (I10)

Enterprises understand "research pod" and "decision board", not contextScope: OWN_FEEDBACK. Packaged templates ship complete, validated group configs whose member slots are named roles — instantiation just assigns agents (or human principals) to them and saves through the normal store path, so every save-time validation applies.

MCP: list_group_templates, create_group_from_template. Missing or unknown roles fail loudly, naming the template's real roles.

Template
Style
What it packages

research-pod

DELPHI-style CUSTOM

Blind estimates → anonymous convergence rounds (I2) → synthesis → retro into team memory (I8), windowed context (I9), dollar ceiling (I1)

editorial-team

CUSTOM

Writer drafts a shared artifact (I17), editors propose CAS-guarded updates, dissents recorded (I4)

ops-task-force

TASK_FORCE

Bid-based assignment (I18), agent-filed tasks (I5), specialist recruitment (I7), ceiling

decision-board

CUSTOM

Hybrid board with a HUMAN director (I6) deliberating and voting (I14); options distilled by the chair, ties to the chair

negotiation-table

NEGOTIATION

Typed two-party bargaining with concession ledger (I11); the arbiter role may be a human principal

Attachments

A discussion can carry shared files. POST /groups/{groupId}/conversations (and the /stream variant) accepts an attachments array alongside question, in two shapes — hosted (mimeType + url) and inline (mimeType + data, optionally fileName):

Inline data is stored in the blob store owned by the group conversation, so it is granted to members and reaped with the conversation. Hosted url references pass through as-is. A ref carrying neither is skipped, as is a url ref with no mimeType.

How members receive them. On a member's first turn the orchestrator grants that member's private conversation access to the group's blobs and injects them as attachment_* context — from there the ordinary single-agent attachment path applies (multimodal forwarding for vision models, PDF/text extraction otherwise). On later turns the member's own conversation history carries them: AttachmentForwarder notes the earlier attachments and the readAttachment tool is auto-enabled for any conversation that has them, independently of builtInToolsWhitelist. A recruited member gets the same grant on its own first turn, and a nested GROUP member propagates the whole set down.

Bounds worth knowing:

  • The per-turn forwarding cap (eddi.attachments.max-per-turn, default 5) applies to each member turn. A discussion sharing more than that will have the surplus dropped for that turn, reported in the member's attachments:errors — visible in that member's conversation, not in the group transcript.

  • url-only attachments are not blob-backed and are not re-hydrated after a HITL resume; blob-backed ones are.

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.

Depth tracking prevents infinite recursion (eddi.groups.max-depth, default: 3). A group that would contain itself through its GROUP members (directly or via other groups) is rejected when it is saved.

Custom Phases

For full control, define phases directly:

A CRITIQUE phase with targetEachPeer: true has every member critique each peer in turn. Without it, each member reviews all peers' latest responses in one turn.

Per-phase controls

Beyond type/participants/turnOrder/contextScope, a phase carries four controls that shape when it stops and what it costs:

repeats runs the same phase N times over the growing transcript — the mechanism behind DELPHI's rounds.

requiresApproval pauses the discussion at the phase boundary until a human approves or rejects (see HITL). At TASK granularity on an EXECUTE phase it pauses per submitted task instead.

convergence (I2) ends a repeating phase early once the round stopped producing movement, so a group that agreed after round 2 does not pay for round 5. A judge compares this round's contributions against the previous round's and scores similarity against threshold; minRepeats (minimum 2 — one round has nothing to compare against) guards against calling convergence before there is evidence. judge selects who scores it: MODERATOR runs the moderator agent as the judge (in its own conversation, so the judge's JSON-only prompts never leak into a later SYNTHESIS turn). SERVICE is accepted but not yet wired — it logs a warning and falls back to the same moderator-agent path, so it currently costs the same as MODERATOR rather than the cheaper dedicated call the name suggests. Either way, a group with no moderator skips the judge and the phase simply keeps going. Converging is a real completed repeat: it still persists, still fires onPhaseComplete, and still runs the phase's decision block.

allowAbstention (I4) appends an instruction telling members they may reply with the single token PASS when they have nothing to add. A passing member produces an ABSTAINED entry carrying no content: peers, the synthesizer and the convergence judge never see it, so a late DELPHI round costs one short call instead of three agents restating agreement in prose. The entry is still counted — a round in which every scheduled participant abstained ends the phase through the unanimous-abstention exit. Task phases never advertise the token — the detector and the prompt read the same switch, so they cannot disagree.

Dissent (I4)

"recordDissents": true on the group gives every participant who did not write a synthesis one short turn to state where they still materially disagree. Non-PASS replies become public DISSENT transcript entries and populate decision.dissents.

It is opt-in because it costs one extra short call per non-synthesizer. It is nonetheless the honest design: the alternative is asking the synthesizer to report the objections to its own synthesis, which is precisely the failure mode a minority report exists to prevent.

Phase Types

Type
Purpose

OPINION

Share perspective on the question

CRITIQUE

Review another member's response

REVISION

Revise own response based on feedback

CHALLENGE

Argue against consensus (devil's advocate)

DEFENSE

Defend position against challenges

ARGUE

Present argument for a side (debate)

REBUTTAL

Counter opposing arguments

PLAN

Decompose the question into sub-tasks

EXECUTE

Work on assigned sub-task

VERIFY

Review and validate another member's work

SYNTHESIS

Moderator produces balanced conclusion

VOTE

Collect explicit ballots and tally a decision (I14)

PROPOSAL

State an opening offer in a negotiation (I11)

BARGAIN

Counter, concede, or sign an acceptance (I11)

RETRO

Draw process lessons into team memory (I8)

Context Scopes

Scope
What the agent sees

NONE

Only the question (independent)

FULL

All previous transcript entries

LAST_PHASE

The previous phase's entries and the current phase's so far — in a sequential phase, that is what lets the second speaker react to the first

ANONYMOUS

Previous entries with speaker names removed

OWN_FEEDBACK

Only feedback addressed to this agent

TASK_ONLY

Only this agent's assigned task from the plan

TASK_WITH_DEPS

Assigned task plus outputs from dependency tasks

TASK_FORCE Configuration

The TASK_FORCE style uses a 4-phase pipeline: Plan → Execute → Verify → Synthesize.

  1. PLAN — The moderator decomposes the goal into actionable tasks and assigns each to an agent

  2. EXECUTE — Agents execute their assigned tasks in parallel (each sees only TASK_ONLY or TASK_WITH_DEPS context)

  3. VERIFY — The moderator reviews each task result against the original goal

  4. SYNTHESIS — The moderator combines all verified results into a coherent final deliverable

Pre-Configured Tasks

Pass a tasks array to skip the PLAN phase entirely — useful for deterministic, repeatable workflows:

When tasks is provided, the system posts [System] "Pre-configured task plan: N tasks" instead of invoking the moderator's LLM. A CUSTOM group whose phases include EXECUTE but no PLAN gets the same materialization at the start of EXECUTE. A requiresApproval gate on a PLAN phase does not apply there, because there is no plan phase to pause after.

Task Dependencies

Use dependsOn to create sequential execution chains. Each entry references a task subject:

  • Tasks with no dependencies execute in parallel

  • Tasks with dependencies wait for their predecessors to complete

  • Dependent tasks receive their predecessor's output via the TASK_WITH_DEPS context scope

  • Cycle detection prevents circular dependency chains (fails fast at planning time)

Task Statuses

Status
Meaning

PENDING

Waiting for dependencies or execution

ASSIGNED

Assigned to an agent, waiting to start

IN_PROGRESS

Currently being executed by an agent

COMPLETED

Agent produced output

VERIFIED

Moderator verified the result

BLOCKED

Reserved for dependency/resource blocking — recognized as non-terminal, but the engine does not set it yet

AWAITING_APPROVAL

Result submitted, waiting on a human (TASK-granularity HITL)

FAILED

Agent or verification failed

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:

  • 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.

Dynamic Agents

During TASK_FORCE (or any group) discussions, agents with the appropriate LLM tools can create, recruit, and delegate to new agents at runtime:

Tool
Purpose

CreateSubAgentTool

Create a new ephemeral agent with a specific system prompt

ConverseWithAgentTool

Delegate a sub-task to an existing deployed agent

FindAgentsByCapabilityTool

Discover agents by capability keywords

RecruitAgentTool

Bring a discovered agent into the discussion as a member

TeardownAgentTool

Clean up dynamically created agents

Recruitment

Discovery and recruitment are two halves of one capability, and both are gated by allowRecruitment. findAgentsByCapability locates a specialist; recruitAgent(agentId, role, reason) brings it in.

A recruit joins from the next round, never mid-round — a roster that changed while a round was running would move the speaker index a paused discussion resumes from, and change the denominator the convergence and unanimity checks already computed for the round in flight. The recruitment is recorded as a FACILITATION transcript entry naming the recruiter, the recruit, the role and the reason, so the rest of the team can see why the roster changed.

Recruitment is refused, with an actionable message, when the agent is not deployed, is already a member, is the recruiter itself, when maxRecruitedAgentsPerDiscussion is reached, or when the discussion's owner may not use the agent. A recruit speaks as that owner and at their cost, so with workspaces enforced it has to be an agent the owner could have started a conversation with: their own, one shared with them directly, or a published one. Team shares do not count here — the member turn runs without the owner's token, so their team memberships are unknown. An owner who holds eddi-admin on the request that drives the turn is admitted, as they are everywhere else. The access check runs before the deployment check, so a refusal never reveals whether an off-limits agent is deployed.

Recruits are never torn down. They are pre-existing deployed agents the discussion borrowed, so TeardownAgentTool and end-of-discussion cleanup leave them alone — undeploying one would take it away from every other conversation using it. Only agents the discussion created are cleaned up.

DynamicAgentConfig

Guardrails for dynamic agent creation are configured per-group via AgentGroupConfiguration.dynamicAgents:

Setting
Default
Purpose

enabled

false

Master switch for dynamic agent capabilities

allowCreation

false

Allow creating new agents (vs. only recruiting existing)

allowRecruitment

false

Allow recruiting already-deployed agents into the discussion

allowDelegation

true

Allow delegating sub-tasks to other agents

maxCreatedAgentsPerDiscussion

5

Cap on new agents created per discussion — counted across all members, not per member; a torn-down agent frees its slot

maxRecruitedAgentsPerDiscussion

10

Cap on recruited agents per discussion

delegationTimeoutSeconds

60

How long a delegating agent waits for its delegate's turn. Non-positive falls back to the default

maxDelegationsPerTask

3

Cap on delegations per task

maxDelegationDepth

3

How deep a delegation chain may nest before it is refused

allowedDelegationTargets

null (any deployed agent)

Whitelist of agent ids a member may delegate to

lifecyclePolicy

EPHEMERAL

EPHEMERAL, KEEP_DEPLOYED, UNDEPLOY_ONLY, or AGENT_DECIDES

inheritParentModel

true

Created agents inherit the parent's provider, model and API-key reference — see below

allowedProviders

null (any)

Whitelist of LLM providers

allowedModels

null (any)

Per-provider model whitelist

Dynamic agents are tracked in GroupConversation.dynamicMembers, createdAgentIds, and retainedAgentIds.

What the tools may touch.

  • teardown_agent undeploys (and optionally deletes) only an agent that is both in the conversation's created list and carries a dynamicOrigin in its own configuration naming the calling conversation or its discussion. create_sub_agent stamps that marker on the agent's first version; an agent a person built has none, so it can never be torn down by a tool. Only the engine writes the marker: an agent update (PUT, a merge import, an upgrade) keeps the stored value whatever the body says, and a duplicate or a ZIP import that creates a new agent drops it.

  • End-of-discussion cleanup applies the same marker: it deletes only agents whose dynamicOrigin names this discussion, leaves agents marked for another one alone, and only undeploys — never deletes — an agent with no marker (one created before markers existed, or never created by create_sub_agent).

  • retain_agent flags persist across turns; unretain_agent removes them for good.

  • converse_with_agent continues only a conversation owned by the same user the calling conversation belongs to (a conversation of another user, or one that records no owner, is refused). Every delegation — a new conversation or a continued one — requires that user to be allowed to use the target agent, the same rule as recruitment above; agents this conversation or its discussion created are exempt — "created" meaning tracked and carrying a dynamicOrigin that names this conversation or its discussion.

Model and credential inheritance

createSubAgent takes provider and model as optional arguments. What is not given is inherited from the creating agent, in this order:

  1. Provider — the parent's, when inheritParentModel is on.

  2. Model — the parent's, but only if the effective provider is still the parent's. An anthropic parent creating an ollama sub-agent does not hand it a Claude model name.

  3. API key — the parent's ${vault:...} reference, again only when the provider matches, because a vault reference names one provider's secret.

Only a vault reference is inherited, never a plaintext key. When the vault is unconfigured, vaultApiKey falls back to storing the key in clear; copying such a value into a second config would multiply that fallback's blast radius. A parent whose key is plaintext therefore inherits nothing, and creation fails with the ordinary "API key is required" error.

The allow-lists are checked against the effective provider and model — the values after inheritance and defaults are applied, not the raw arguments. Omitting provider does not skip allowedProviders. For allowedModels:

  • Provider named (or inherited): an absent or empty entry for that provider means no restriction for that provider.

  • No provider named: the model is about to be paired with the default provider, so that provider must itself appear in allowedModels. A config restricting openai to gpt-4o-mini will not silently build an anthropic agent running gpt-4o-mini.

Tenant Quota Enforcement

If tenant quotas are enabled, QuotaExceededException is propagated regardless of the group's onAgentFailure policy — quota violations always abort the discussion to prevent runaway resource consumption.

Protocol Configuration

Setting
Options
Default

agentTimeoutSeconds

Any positive integer

180

onAgentFailure

SKIP, RETRY, ABORT

SKIP

maxRetries

0+

2

onMemberUnavailable

SKIP, FAIL

SKIP

maxTurns

Any positive integer (0 or negative → default)

50

maxCostPerDiscussion

Dollars, or null for unlimited

null

onCostExceeded

SYNTHESIZE_NOW, ABORT

SYNTHESIZE_NOW

The two ceilings are different instruments. maxTurns bounds how many member turns a discussion may spend — it is the structural stop that keeps a recruiting, extending, re-planning discussion finite. maxCostPerDiscussion bounds dollars, which is what actually differs between a cheap 40-turn round table and an expensive 8-turn one with large contexts. Hitting the cost ceiling under SYNTHESIZE_NOW skips the remaining discussion phases and jumps straight to synthesis, so the caller still gets an answer for what was already spent; ABORT fails the discussion instead. Every LLM call in the discussion is attributed, including the facilitator's, the convergence judge's, and the summarizer's.

The defaults above apply when the protocol block is omitted entirely, not only when an individual setting is. Nothing backfills a stored config, so a group saved without a protocol block runs on exactly these values.

Timeout guidance: 180s covers thinking models (e.g. claude-sonnet-5) and synthesis phases comfortably. For tool-calling agents with multiple tool loops, consider 300–600. The timeout is per agent turn, not per phase.

REST API

Method
Path
Description

POST

/groupstore/groups

Create group config

GET

/groupstore/groups/descriptors

List group configs (filter, index, limit)

GET

/groupstore/groups/{id}

Read group config

PUT

/groupstore/groups/{id}

Update group config

DELETE

/groupstore/groups/{id}

Delete group config

GET

/groupstore/groups/styles

List discussion styles

POST

/groups/{groupId}/conversations

Start discussion

GET

/groups/{groupId}/conversations/{id}

Read transcript

GET

/groups/{groupId}/conversations

List conversations

DELETE

/groups/{groupId}/conversations/{id}

Delete (artifacts + ephemeral agents; members ended)

GET

/groupstore/groups/jsonSchema

JSON schema for the group config

POST

/groups/{groupId}/conversations/stream

Start discussion, stream events over SSE

POST

/groups/{groupId}/conversations/{id}/followup

Ask one member a follow-up

POST

/groups/{groupId}/conversations/{id}/continue

Continue with a new question (same transcript)

POST

/groups/{groupId}/conversations/{id}/continue/stream

Continue, streaming

POST

/groups/{groupId}/conversations/{id}/close

Close the conversation to further rounds

POST

/groups/{groupId}/conversations/{id}/cancel

Cancel a running discussion

POST

/groups/{groupId}/conversations/{id}/approve

Approve/reject a HITL pause. Returns as soon as the decision is recorded — the resumed run continues asynchronously, so the response typically shows IN_PROGRESS; poll the conversation or use /approve/stream

POST

/groups/{groupId}/conversations/{id}/approve/stream

Approve and stream the resumed run

POST

/groups/{groupId}/conversations/{id}/human-input

Submit a HUMAN member's turn (I6). Asynchronous like /approve: the discussion resumes in the background (IN_PROGRESS)

GET

/groups/{groupId}/conversations/{id}/approval-status

Pause coordinates (detail=full for approvers)

GET

/groups/{groupId}/conversations/pending-approvals

This group's discussions awaiting a decision

GET

/groups/pending-approvals

Every group discussion awaiting a decision, across all groups

GET

/groupstore/templates

List preset templates (I10)

GET

/groupstore/templates/{templateId}

Read one template

POST

/groupstore/templates/{templateId}/instantiate

Create a group from a template

GET

/groupstore/groups/{id}/workspace

Read the standing-team workspace (I13)

GET

/groupstore/groups/{id}/workspace/backlog

Read the team backlog

POST

/groupstore/groups/{id}/workspace/backlog

File a backlog task

POST

/groupstore/groups/{id}/workspace/cadences

Add a cron cadence (409 if the workspace keeps changing under three revision-checked attempts; the schedule it created is deleted again)

DELETE

/groupstore/groups/{id}/workspace/cadences/{cadenceId}

Remove a cadence, then its schedule (409 as above; cadence and schedule then both stay)

SSE Events

POST /groups/{groupId}/conversations/stream (and the continue/approve streaming variants) emit these events. A client that only cares about the answer can watch group_complete; a UI rendering the discussion live wants the speaker and phase pairs.

Event
Fires when

group_start

The discussion begins (carries group id and question)

phase_start / phase_complete

A phase opens / closes

round_start

A continuation round (round 2+) of the whole discussion begins — see POST .../continue. Phase repeats fire phase_start instead

speaker_start / speaker_complete

A member's turn opens / closes. complete carries the content, or — when the turn produced none — no content and an outcome of TIMEOUT, SKIPPED or ERROR. Every started turn is closed, including a parallel member released by the batch deadline

token

Incremental token from a streaming member turn

synthesis_start / synthesis_complete

The synthesis phase opens / closes

convergence_checked / convergence_reached

A convergence judge ran / declared the phase converged (I2)

decision_reached

A DecisionRecord was set — vote tally, debate verdict, or negotiation agreement (I14/I11)

task_plan_created / task_verified

TASK_FORCE planned its backlog / verified a task

retro_recorded

A RETRO phase finished, with the number of lessons stored (I8)

artifact_updated

A shared artifact was created or revised (I17)

human_input_requested

A HUMAN member's turn is pending (I6)

awaiting_approval / hitl_resume

A HITL pause was committed / released

member_pause_skipped

A member turn was skipped rather than paused

cancelled

The discussion was cancelled

group_complete

Terminal — carries the final state and the synthesized answer

group_error

Terminal failure, with a curated (never raw) message

MCP Tools

Group-related tools live in two classes: the group tools below, plus six group-scoped tools provided by the HITL tool set (marked) — three that act on a pause and three that find one. Both are exposed on the same MCP server; the split is internal.

Tool
Description

describe_discussion_styles

Rich descriptions of all styles

list_groups

List group configs

read_group

Read group config

create_group

Create group (name, members, style, roles, types)

update_group

Update group config JSON

delete_group

Delete group config

discuss_with_group

Start discussion, return transcript

read_group_conversation

Read conversation transcript

list_group_conversations

List past discussions for a group, with state and timestamps

start_group_discussion

Start a discussion asynchronously (returns immediately). Poll with read_group_conversation

delete_group_conversation

Delete a group conversation. Artifacts and ephemeral agents are deleted; member conversations are ended, not deleted

followup_with_member

Ask a single member a follow-up on a finished discussion

continue_group_discussion

Continue a discussion with a new question

close_group_conversation

Close a conversation to further rounds

list_group_pending_approvals

List one group's discussions awaiting a decision — HITL tool set

list_all_group_pending_approvals

The cross-group approval inbox — HITL tool set

get_group_approval_status

Pause coordinates for one discussion — HITL tool set

submit_group_human_input

Submit a HUMAN member's turn (I6) — HITL tool set

approve_group_phase

Approve or reject a HITL pause — HITL tool set

cancel_group_discussion

Cancel a running discussion — HITL tool set

add_team_task

File a task on a standing team's backlog (I13)

list_team_backlog

Read a standing team's backlog

list_group_templates

List the preset group templates (I10)

create_group_from_template

Create a group from a template + role assignments

Slack Integration

Group discussions integrate natively with Slack. See slack-integration.md for full setup instructions.

UX Pattern: Header + Thread

All discussion styles use the same rendering pattern in Slack:

  1. Start Banner — posted in the user's thread with style name, agent count, and question

  2. Agent Headers — each agent's first contribution is a channel-level message with a short preview

  3. Full Content — the complete response is posted as a thread reply under the agent's header

  4. Peer Feedback — feedback threads under the target agent's header message

  5. Revisions — revised contributions thread under the agent's own header

  6. Synthesis — moderator's synthesis gets its own channel-level header + thread

Discussion Styles in Slack

Style
Phase Flow in Slack

ROUND_TABLE

Each agent posts → Moderator synthesizes

PEER_REVIEW

Agents post → Critiques thread under targets → Revisions thread under own → Synthesis

DEVIL_ADVOCATE

Agent posts → Challenger threads challenges → Agent threads defense → Synthesis

DEBATE

PRO agent posts → CON agent posts → Rebuttals thread under opponents → Judge synthesizes

DELPHI

Round 1 agents post → Round 2 agents post (convergence) → Synthesis

TASK_FORCE

Moderator posts plan → Agents post task results → Verifiers thread under targets → Synthesis

Trigger Keywords

Configure trigger keywords in ChannelIntegrationConfiguration to route to specific groups:

Follow-up Conversations

After a discussion, users can reply in any agent's thread to ask follow-up questions. The system injects the agent's discussion context (contribution + peer feedback received) into the prompt for a contextual response.

Not yet supported

Known bounds, so they are not discovered at runtime:

Area
Behaviour today

Member-level tool approval

A member agent's hitlConfig.toolApprovals does not gate inside a group. The framework auto-rejects the gated call (system:group) and the member produces a tool-less contribution — see hitl.md → Group members. inGroupTurns: "INBOX" is reserved and rejected at save time.

Nested pauses

A sub-group that pauses for approval or a human turn is cancelled and its member turn recorded SKIPPED. Nested HITL is not supported.

Groups over the OpenAI-compatible API

/v1 exposes deployed agents as models. A group is not addressable there; use the REST or MCP surfaces.

Groups over A2A

An A2A peer can invoke an agent, not a group.

Cross-node tool availability

The task, artifact and recruit tools resolve the running discussion from an in-process registry. Member turns always run in the same JVM as their orchestrator, so this holds — but a discussion is only "live" on the node running it.

Configuration

A sub-agent that inherits a parent's vault reference must itself be granted that secret, or — under the default eddi.vault.grant-enforcement=enforce — it will not deploy. See secrets-vault.md → Agent Grants.

Last updated

Was this helpful?