import type { Runner } from "../core/runner/runtask.js"; import type { McpServerSpec, ModelRef, ModelRole, TaskResult, ToolSpec } from "../core/types.js"; export interface TeamMember { /** The member's role / specialty, e.g. "安全评审" or "performance". */ role: string; /** Optional per-member model (falls back to the team default). */ model?: ModelRef; /** Optional per-member model **role** (when `model` is omitted) — for per-member cost tiering, e.g. * a "researcher" on `"subagent"` (cheap) and a "critic" on `"default"`. Default `"team"`. */ modelRole?: ModelRole; /** Optional persona / standing instructions for this member. */ systemPrompt?: string; /** * Tools this member may use mid-debate to **ground or verify** a claim against the real artifacts * (e.g. a read-only repo tool) instead of speculating — tool-augmented debate (the "Tool-MAD" * pattern). Falls back to the team's shared {@link TeamDiscussionOptions.tools}. A member's turn * becomes a full agent loop, so it is bounded by `limits` (and core's default turn cap). */ tools?: ToolSpec[]; /** * Per-member tool allow/deny (design/38 1A), applied to this member's resolved tools (own or the * team default): `allowTools` keeps only the named tools (`["*"]` = all), then `denyTools` removes. * Lets a "researcher" be read-only and an "implementer" carry write tools without separate roles. */ allowTools?: string[]; denyTools?: string[]; /** Task-scoped MCP servers for this member; falls back to the team's shared `mcp`. */ mcp?: McpServerSpec[]; } export interface TeamTurn { round: number; role: string; text: string; /** True when this member turn failed (retries exhausted). Surfaced in the result for visibility, * but **excluded from every prompt** so an `[unavailable]` marker can't pollute the discussion. */ failed?: boolean; } export type TeamEvent = { type: "member_start"; round: number; role: string; } | { type: "member_end"; round: number; role: string; text: string; } /** [571]③ budget axes: the cumulative team budget was exhausted after this member's turn settled — * remaining rounds/members are skipped and the discussion goes straight to synthesis. */ | { type: "budget_stop"; axis: "maxTokens" | "maxCostUsd"; round: number; role: string; } | { type: "synthesis_start"; } | { type: "done"; }; export interface TeamDiscussionOptions { runner: Runner; /** Default model for members / synthesizer without their own. If omitted, members resolve the * `team` role and the synthesizer the `synthesize` role (both fall back to `default`). */ model?: ModelRef; /** The participants. */ members: TeamMember[]; /** What the team is discussing / deciding. */ topic: string; /** Number of discussion rounds before synthesis. Default 2. */ rounds?: number; /** * Cap on the shared transcript embedded in each prompt. When the running transcript exceeds * this many estimated tokens, the oldest statements are summarized into a running summary * (keeping prompts bounded over many rounds). Omit to disable. */ maxTranscriptTokens?: number; /** * Default tools for members that don't declare their own — e.g. a read-only repo tool so the * debate can **verify claims against the real artifacts** rather than argue over unseen code. * Members without tools simply reason over the shared transcript (the convergence layer). */ tools?: ToolSpec[]; /** Default MCP servers for members without their own. */ mcp?: McpServerSpec[]; /** * Who synthesizes the conclusion (default: a neutral facilitator on the default model). The * synthesizer may carry its own `tools`/`mcp` to spot-check a disputed point before deciding; by * default it has none (it converges over the transcript, it doesn't re-explore). */ synthesizer?: { role?: string; model?: ModelRef; modelRole?: ModelRole; systemPrompt?: string; /** Tools specific to the synthesizer; does NOT fall back to the team-level `opts.tools`. */ tools?: ToolSpec[]; /** Per-agent tool allow/deny (design/38 1A), applied to the synthesizer's `tools`. */ allowTools?: string[]; denyTools?: string[]; mcp?: McpServerSpec[]; }; /** Progress callback. */ onEvent?: (e: TeamEvent) => void; /** * `timeoutSec`/`maxTurns` are PER-RUN caps forwarded to every member/summary/synthesizer run. * * `maxTokens`/`maxCostUsd` ([571]③, CollabTemplate.budget mid-flight enforcement) are CUMULATIVE * team budgets over member + summary + synthesizer spend (nested/delegated spend included, same * coordinate as the `stats` totals). Enforcement is checked after each member turn settles and is * booked — the crossing member is never killed in flight — and once a budget is exhausted * (running total ≥ budget) no further member turn or round is dispatched. The transcript produced * so far still goes through the NORMAL synthesis close-out (money already spent must yield a * conclusion), so the synthesizer always runs; its spend is booked but not gated. Attribution * lands in {@link TeamResult.budgetStop} plus a `budget_stop` event. `maxCostUsd` is compared in * integer micro-USD (the engine's `stats.costMicroUsd` coordinate — no float equality); runs that * report no cost (no `model.cost`/pricing) book 0 toward it. Both optional — omitted ⇒ prior * behavior byte-for-byte. The budget axes are NOT forwarded to the nested member specs (a * member's own per-task budget is `TaskSpec.maxTokens`/`maxCostUsd`, a different contract). */ limits?: { timeoutSec?: number; maxTurns?: number; maxTokens?: number; maxCostUsd?: number; }; /** * External cancellation. When it aborts, in-flight member/synthesizer/summary runs are cancelled * (each member turn is a nested `runTask` given this signal) — so a team abort cascades to members * instead of each running to its own `timeoutSec`. Pass a parent tool's `ToolExecuteContext.signal`. */ signal?: AbortSignal; /** * Authenticated end-user {@link TaskSpec.principal} (design/62) propagated to every member / summarizer / * synthesizer run, so their MCP tools carry the per-user identity. The orchestrator sets it from the trusted * task context (a team is a control-plane composition, so it is set here rather than auto-inherited). */ principal?: string; } export interface TeamResult { /** * design/80 D-G data contract: the UNIFORM orchestrator-outcome projection. `team` was the only * orchestrator exit whose result didn't expose `status`/`errorCode`/`result` (cascade/teacher/verify * all `extends TaskResult`), forcing aggregators onto a special-case code path. These three projection * fields close that gap so a consumer can read the SAME failure-class fields off every orchestrator exit * (e.g. `errorClassOf(r.errorCode)`). * * This is a PROJECTION, not `extends TaskResult`: a team is a multi-member discussion with **no single** * `taskId`/`sessionId`/`stats` shape (its `stats` is `{tokens,turns}` only, and the rich per-task * `costMicroUsd`/`nested`/`humanReview` lines have no team-level meaning), so forcing those required * TaskResult fields would invent semantically-wrong values. We project ONLY the three fields aggregators * read. All are purely ADDITIVE — existing consumers (`conclusion`/`conclusionValid`/`transcript`/`stats`) * are untouched. * * `status` is `"completed"` on a valid synthesis, `"failed"` when synthesis failed or a member durably * paused; `result` mirrors `conclusion`; `errorCode` is set only on the durable-pause exit * (`unexpected.suspended` / `unexpected.needs_review`, the same hard-boundary codes the other * orchestrators use via {@link mapNestedSuspend}). */ status: "completed" | "failed"; /** Mirrors {@link conclusion} — the TaskResult-shaped `result` accessor for uniform aggregation. */ result: string; /** The failure CLASS code (foldable with `errorClassOf`); set only when a member durably paused * (`unexpected.suspended` / `unexpected.needs_review`). Undefined on a normal completion/synthesis fail. */ errorCode?: string; conclusion: string; /** False when the synthesizer failed and `conclusion` is an `[unavailable…]` marker, not a real * conclusion — so callers can tell a junk conclusion from a legitimate one. */ conclusionValid: boolean; transcript: TeamTurn[]; /** `costMicroUsd` ([571]③): cumulative team LLM spend in integer micro-USD (member + summary + * synthesizer, nested included) — the same engine coordinate as `TaskResult.stats.costMicroUsd`. * Always set (0 when no run reported cost); optional only for type-level back-compat. */ stats: { tokens: number; turns: number; costMicroUsd?: number; }; /** * [571]③ budget-stop attribution: set when a cumulative budget axis was exhausted and the * discussion stopped dispatching further members/rounds early. `round`/`role`/`memberIndex` * identify the LAST member turn that ran (the one whose settled totals crossed the budget); * everything scheduled after it was skipped and the transcript went straight to synthesis. * NOT set when the budget was only crossed by the final scheduled member turn (nothing was * skipped) or by the synthesizer itself — compare `stats` against the budget for that readout. */ budgetStop?: { axis: "maxTokens" | "maxCostUsd"; round: number; role: string; memberIndex: number; }; /** Number of member/synth turns that failed even after a retry (surfaced, not silent). */ failures: number; /** How many times the shared transcript was summarized to stay under maxTranscriptTokens. */ transcriptCompactions: number; /** design/80 D-B (codex review): set when a member durably PAUSED (suspended/needs_review) on a HITL gate — * the discussion STOPS (no synthesis on a half-done team) and surfaces the resume capability so the caller * can resume the paused member via the token, then re-run. `conclusionValid` is false in this case. */ durablePause?: boolean; checkpointToken?: TaskResult["checkpointToken"]; checkpointGate?: TaskResult["checkpointGate"]; } /** * Run a multi-round team discussion across several role-specialized agents, then synthesize a conclusion. * * Each member is shown the shared transcript (quoted as data) plus an explicit instruction block stating * the discussion context, its role, and who else is present. Members are stateless per round (the shared * transcript carries continuity), which keeps the orchestration simple and isolated. */ export declare function runTeamDiscussion(opts: TeamDiscussionOptions): Promise; //# sourceMappingURL=team.d.ts.map