/**
 * @fileoverview Core type definitions for the open-multi-agent orchestration framework.
 *
 * All public types are exported from this single module. Downstream modules
 * import only what they need, keeping the dependency graph acyclic.
 */

import type { ZodSchema } from 'zod'
import type { SupportedProvider } from './llm/adapter.js'

// ---------------------------------------------------------------------------
// Content blocks
// ---------------------------------------------------------------------------

/** Plain-text content produced by a model or supplied by the user. */
export interface TextBlock {
  readonly type: 'text'
  readonly text: string
}

/** Provider-native reasoning / thinking content emitted alongside normal text. */
export interface ReasoningBlock {
  readonly type: 'reasoning'
  readonly text: string
  /**
   * Provider-issued signature proving the reasoning was generated by the
   * model. Used by:
   *   - Anthropic / Bedrock: required to echo back unchanged on the next
   *     turn to continue a multi-turn tool-use conversation with extended
   *     thinking; the API rejects edited or missing signatures.
   *   - Gemini 3: optional `thoughtSignature` attached to thought-summary
   *     Parts; recommended to round-trip per Gemini docs ("always pass all
   *     signatures back as received") but not strictly required.
   * Absent for providers that don't sign reasoning (Gemini 2.5 thought
   * summaries, OpenAI o-series) and for reasoning that originated outside
   * the provider being addressed.
   */
  readonly signature?: string
  /**
   * Opaque encrypted thinking content returned by Anthropic when its safety
   * system blocks the raw reasoning text. The block must still be echoed
   * back on subsequent turns to satisfy the protocol, but its contents are
   * not human-readable. Carried separately from `text` because the wire
   * format for redacted blocks is distinct (`redacted_thinking` vs
   * `thinking`).
   */
  readonly redactedData?: string
  /**
   * Name of the {@link LLMAdapter} that produced this block, stamped at
   * extraction time on the native-to-IR path (both `chat` responses and
   * streaming final payloads). Consumed by outbound IR-to-native conversion
   * to decide between native echo and the `<thinking>` text fallback for
   * adapters whose {@link LLMAdapter.capabilities.echoesReasoning} is
   * `'own-issued'`.
   *
   * Typed as `string` rather than `SupportedProvider` so third-party adapter
   * implementations can stamp their own adapter name without modifying the
   * core enum.
   *
   * Optional for backwards compatibility with externally constructed IR.
   * Outbound paths treat `undefined` as "unknown provenance → cannot
   * native-echo".
   */
  readonly provenance?: string
}

/**
 * A request by the model to invoke a named tool with a structured input.
 * The `id` is unique per turn and is referenced by {@link ToolResultBlock}.
 */
export interface ToolUseBlock {
  readonly type: 'tool_use'
  readonly id: string
  readonly name: string
  readonly input: Record<string, unknown>
  /**
   * Provider-issued signature attached to a tool call to prove the call was
   * generated under extended thinking. Currently only used by Gemini
   * (`thoughtSignature`), which is a top-level field on the Part containing
   * the functionCall — an asymmetry from Anthropic, which signs the
   * preceding {@link ReasoningBlock} instead. Echo back unchanged on
   * subsequent turns to satisfy Gemini 3+'s strict validation.
   */
  readonly signature?: string
}

/**
 * The result of executing a tool, keyed back to the originating
 * {@link ToolUseBlock} via `tool_use_id`.
 */
export interface ToolResultBlock {
  readonly type: 'tool_result'
  readonly tool_use_id: string
  readonly content: string
  readonly is_error?: boolean
}

/** A base64-encoded image passed to or returned from a model. */
export interface ImageBlock {
  readonly type: 'image'
  readonly source: {
    readonly type: 'base64'
    readonly media_type: string
    readonly data: string
  }
}

/** Union of all content block variants that may appear in a message. */
export type ContentBlock = ReasoningBlock | TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock

// ---------------------------------------------------------------------------
// LLM messages & responses
// ---------------------------------------------------------------------------

/**
 * A single message in a conversation thread.
 * System messages are passed separately via {@link LLMChatOptions.systemPrompt}.
 */
export interface LLMMessage {
  readonly role: 'user' | 'assistant'
  readonly content: ContentBlock[]
}

/** Context management strategy for long-running agent conversations. */
export type ContextStrategy =
  | { type: 'sliding-window'; maxTurns: number }
  | { type: 'summarize'; maxTokens: number; summaryModel?: string }
  | {
      type: 'compact'
      /** Estimated token threshold that triggers compaction. Compaction is skipped when below this. */
      maxTokens: number
      /** Number of recent turn pairs (assistant+user) to keep intact. Default: 4. */
      preserveRecentTurns?: number
      /** Minimum chars in a tool_result content to qualify for compaction. Default: 200. */
      minToolResultChars?: number
      /** Minimum chars in an assistant text block to qualify for truncation. Default: 2000. */
      minTextBlockChars?: number
      /** Maximum chars to keep from a truncated text block (head excerpt). Default: 200. */
      textBlockExcerptChars?: number
    }
  | {
    type: 'custom'
    /**
     * Compaction callback. Invoked before every LLM turn including the first,
     * so implementations that should only fire past a token threshold must
     * self-gate inside this function.
     */
    compress: (
      messages: LLMMessage[],
      estimatedTokens: number,
    ) => Promise<LLMMessage[]> | LLMMessage[]
  }

/** Token accounting for a single API call. */
export interface TokenUsage {
  readonly input_tokens: number
  readonly output_tokens: number
}

/** Normalised response returned by every {@link LLMAdapter} implementation. */
export interface LLMResponse {
  readonly id: string
  readonly content: ContentBlock[]
  readonly model: string
  readonly stop_reason: string
  readonly usage: TokenUsage
}

// ---------------------------------------------------------------------------
// Streaming
// ---------------------------------------------------------------------------

/**
 * A discrete event emitted during streaming generation.
 *
 * - `text`        — incremental text delta
 * - `reasoning`   — incremental reasoning / thinking delta
 * - `tool_use`    — the model has begun or completed a tool-use block
 * - `tool_result` — a tool result has been appended to the stream
 * - `budget_exceeded` — token budget threshold reached for this run
 * - `done`        — the stream has ended; `data` is the final {@link LLMResponse}
 * - `error`       — an unrecoverable error occurred; `data` is an `Error`
 */
export interface StreamEvent {
  readonly type: 'text' | 'reasoning' | 'tool_use' | 'tool_result' | 'loop_detected' | 'budget_exceeded' | 'done' | 'error'
  readonly data: unknown
}

// ---------------------------------------------------------------------------
// Tool definitions
// ---------------------------------------------------------------------------

/** The serialisable tool schema sent to the LLM provider. */
export interface LLMToolDef {
  readonly name: string
  readonly description: string
  /** JSON Schema object describing the tool's `input` parameter. */
  readonly inputSchema: Record<string, unknown>
}

/**
 * Context injected into every tool execution.
 *
 * Both `abortSignal` and `abortController` are provided so that tools and the
 * executor can choose the most ergonomic API for their use-case:
 *
 * - Long-running shell commands that need to kill a child process can use
 *   `abortController.signal` directly.
 * - Simple cancellation checks can read `abortSignal?.aborted`.
 *
 * When constructing a context, set `abortController` and derive `abortSignal`
 * from it, or provide both independently.
 */
export interface ToolUseContext {
  /** High-level description of the agent invoking this tool. */
  readonly agent: AgentInfo
  /** Team context, present when the tool runs inside a multi-agent team. */
  readonly team?: TeamInfo
  /**
   * Convenience reference to the abort signal.
   * Equivalent to `abortController?.signal` when an `abortController` is set.
   */
  readonly abortSignal?: AbortSignal
  /**
   * Full abort controller, available when the caller needs to inspect or
   * programmatically abort the signal.
   * Tools should prefer `abortSignal` for simple cancellation checks.
   */
  readonly abortController?: AbortController
  /**
   * Working directory for filesystem-tool sandboxing.
   *
   * - `string` — built-in filesystem tools (`file_read`, `file_write`,
   *   `file_edit`, `grep`, `glob`) require absolute paths and reject paths
   *   that resolve outside this directory (including via symlinks).
   * - `null` — sandbox is explicitly disabled; tools accept arbitrary paths.
   * - `undefined` — falls back to `<process.cwd()>/.agent-workspace`, an
   *   auto-created sandbox subdirectory (sandbox enabled).
   */
  readonly cwd?: string | null
  /** Arbitrary caller-supplied metadata (session ID, request ID, etc.). */
  readonly metadata?: Readonly<Record<string, unknown>>
}

/** Minimal descriptor for the agent that is invoking a tool. */
export interface AgentInfo {
  readonly name: string
  readonly role: string
  readonly model: string
}

/**
 * Minimal pool surface used by `delegate_to_agent` to detect nested-run capacity.
 * {@link AgentPool} satisfies this structurally via {@link AgentPool.availableRunSlots}.
 */
export interface DelegationPoolView {
  readonly availableRunSlots: number
}

/** Descriptor for a team of agents (orchestrator-injected into tool context). */
export interface TeamInfo {
  readonly name: string
  readonly agents: readonly string[]
  /** When the team has shared memory enabled; used for delegation audit writes. */
  readonly sharedMemory?: MemoryStore
  /** Zero-based depth of nested delegation from the root task run. */
  readonly delegationDepth?: number
  readonly maxDelegationDepth?: number
  readonly delegationPool?: DelegationPoolView
  /**
   * Ordered chain of agent names from the root task to the current agent.
   * Used to block `A -> B -> A` cycles before they burn turns against `maxDelegationDepth`.
   */
  readonly delegationChain?: readonly string[]
  /**
   * Run another roster agent to completion and return its result.
   * Only set during orchestrated pool execution (`runTeam` / `runTasks`).
   */
  readonly runDelegatedAgent?: (targetAgent: string, prompt: string) => Promise<AgentRunResult>
}

/**
 * Optional side-channel metadata a tool may attach to its result.
 * Not shown to the LLM — the runner reads it for accounting purposes.
 */
export interface ToolResultMetadata {
  /**
   * Token usage consumed inside the tool execution itself (e.g. nested LLM
   * calls from `delegate_to_agent`). Accumulated into the parent runner's
   * total so budgets/cost tracking stay accurate across delegation.
   */
  readonly tokenUsage?: TokenUsage
}

/** Value returned by a tool's `execute` function. */
export interface ToolResult {
  readonly data: string
  readonly isError?: boolean
  readonly metadata?: ToolResultMetadata
}

/**
 * A tool registered with the framework.
 *
 * `inputSchema` is a Zod schema used for validation before `execute` is called.
 * At API call time it is converted to JSON Schema for {@link LLMToolDef}, unless
 * `llmInputSchema` is set (e.g. MCP tools ship JSON Schema from the server).
 *
 * `outputSchema` is optional runtime validation for `ToolResult.data`.  When
 * set and validation fails, execution returns an error ToolResult instead of
 * propagating invalid output.
 */
export interface ToolDefinition<TInput = Record<string, unknown>> {
  readonly name: string
  readonly description: string
  readonly inputSchema: ZodSchema<TInput>
  /**
   * Optional runtime validator for `ToolResult.data` (always a string).
   *
   * **Not to be confused with {@link AgentConfig.outputSchema}**, which
   * validates an agent's final JSON answer. This one only guards a single
   * tool's serialised output.
   */
  readonly outputSchema?: ZodSchema<string>
  /**
   * When present, used as {@link LLMToolDef.inputSchema} as-is instead of
   * deriving JSON Schema from `inputSchema` (Zod).
   */
  readonly llmInputSchema?: Record<string, unknown>
  /**
   * Per-tool maximum output length in characters. When set, tool output
   * exceeding this limit is truncated (head + tail with a marker in between).
   * Takes priority over {@link AgentConfig.maxToolOutputChars}.
   */
  readonly maxOutputChars?: number
  execute(input: TInput, context: ToolUseContext): Promise<ToolResult>
}

// ---------------------------------------------------------------------------
// Agent
// ---------------------------------------------------------------------------

/**
 * Extended thinking / reasoning configuration shared by {@link AgentConfig}
 * and {@link LLMChatOptions}.
 *
 * - `budgetTokens` maps to Anthropic's `thinking.budget_tokens` and Gemini's
 *   `thinkingConfig.thinkingBudget`.
 * - `effort` maps to OpenAI o-series / gpt-5 `reasoning_effort`. Carried as
 *   an explicit value rather than derived from `budgetTokens` so the call
 *   site stays unambiguous across providers.
 *
 * The `effort` union is intentionally narrowed to the values declared by
 * the pinned `openai` SDK (`'low' | 'medium' | 'high'`). Newer values
 * shipped by the API but not yet in the SDK type union (e.g. gpt-5's
 * `'minimal'`, GPT-5.5's `'none'`) should be passed via `extraBody:
 * { reasoning_effort: '<value>' }`, matching how `top_k` / `min_p` are
 * handled for vLLM.
 *
 * Adapters that don't recognise a given field ignore it.
 */
export interface ThinkingConfig {
  readonly enabled: boolean
  readonly budgetTokens?: number
  readonly effort?: 'low' | 'medium' | 'high'
}

/** Context passed to the {@link AgentConfig.beforeRun} hook. */
export interface BeforeRunHookContext {
  /** The user prompt text. */
  readonly prompt: string
  /** The agent's static configuration. */
  readonly agent: AgentConfig
}

/** Static configuration for a single agent. */
export interface AgentConfig {
  readonly name: string
  readonly model: string
  /**
   * Optional custom {@link LLMAdapter}. When set, the agent uses this adapter
   * directly and skips {@link createAdapter} — `provider`, `apiKey`, `baseURL`,
   * and `region` are ignored for LLM calls.
   */
  readonly adapter?: LLMAdapter
  readonly provider?: SupportedProvider
  /**
   * Custom base URL for OpenAI-compatible APIs (Ollama, vLLM, LM Studio, etc.).
   * Note: local servers that don't require auth still need `apiKey` set to a
   * non-empty placeholder (e.g. `'ollama'`) because the OpenAI SDK validates it.
   */
  readonly baseURL?: string
  /** API key override; falls back to the provider's standard env var. */
  readonly apiKey?: string
  /** AWS region override for the `bedrock` provider; falls back to `AWS_REGION` env var, then `'us-east-1'`. Ignored by all other providers. */
  readonly region?: string
  readonly systemPrompt?: string
  /**
   * Custom tool definitions to register alongside built-in tools.
   * Created via `defineTool()`. Custom tools bypass `tools` (allowlist)
   * and `toolPreset` filtering, but can still be blocked by `disallowedTools`.
   *
   * Tool names must not collide with built-in tool names; a duplicate name
   * will throw at registration time.
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  readonly customTools?: readonly ToolDefinition<any>[]
  /** Names of tools (from the tool registry) available to this agent. */
  readonly tools?: readonly string[]
  /** Names of tools explicitly disallowed for this agent. */
  readonly disallowedTools?: readonly string[]
  /** Predefined tool preset for common use cases. */
  readonly toolPreset?: 'readonly' | 'readwrite' | 'full'
  /**
   * Root directory used by built-in filesystem tools (`file_read`,
   * `file_write`, `file_edit`, `grep`, `glob`). Paths must be absolute and
   * resolve inside this directory; symlinks are resolved before the check.
   *
   * Defaults to {@link OrchestratorConfig.defaultCwd} (or
   * `<process.cwd()>/.agent-workspace` if neither is set). Pass `null` to
   * disable the sandbox for this agent.
   */
  readonly cwd?: string | null
  readonly maxTurns?: number
  readonly maxTokens?: number
  /** Maximum cumulative tokens (input + output) allowed for this run. */
  readonly maxTokenBudget?: number
  /** Optional context compression policy to control input growth across turns. */
  readonly contextStrategy?: ContextStrategy
  readonly temperature?: number
  /**
   * OpenAI-track sampling penalty for token frequency. Forwarded to OpenAI
   * cloud and OpenAI-compatible local servers (vLLM, llama-server). The
   * Anthropic adapter ignores this field.
   */
  readonly frequencyPenalty?: number
  /**
   * OpenAI-track sampling penalty for token presence. Forwarded to OpenAI
   * cloud and OpenAI-compatible local servers. The Anthropic adapter
   * ignores this field.
   */
  readonly presencePenalty?: number
  /**
   * Nucleus sampling cutoff. Forwarded to all adapters (OpenAI cloud,
   * OpenAI-compatible local, Anthropic).
   */
  readonly topP?: number
  /**
   * Top-k sampling cutoff. Forwarded to Anthropic and OpenAI-compatible
   * local servers. Cloud OpenAI rejects this parameter.
   */
  readonly topK?: number
  /**
   * Min-p sampling cutoff. Only supported by OpenAI-compatible local
   * servers (vLLM, llama-server, etc.). Cloud OpenAI rejects this parameter
   * and the Anthropic adapter ignores it.
   */
  readonly minP?: number
  /**
   * Whether the model may emit multiple tool calls in a single assistant
   * turn. Forwarded to OpenAI cloud and OpenAI-compatible local servers as
   * `parallel_tool_calls`. The Anthropic adapter ignores this field.
   *
   * - `undefined` (default): omit the field, server default applies. Cloud
   *   OpenAI defaults to `true`.
   * - `false`: force serial tool calling. Required by some local servers
   *   (vLLM, llama-server, certain quantized setups) that truncate
   *   concurrent `tool_call` deltas during streaming or return malformed
   *   `tool_calls` when more than one is emitted.
   * - `true`: explicit opt-in (matches the cloud default; no behavior
   *   change for cloud OpenAI users).
   */
  readonly parallelToolCalls?: boolean
  /**
   * Adapter-specific escape hatch merged into the outgoing request payload.
   * Spread between the sampling params and the structural fields, so values
   * here can override the standard sampling defaults (`temperature`, `topP`,
   * etc.) but cannot override transport-level fields (`model`, `messages`,
   * `tools`, `stream`, and Anthropic's `system`).
   */
  readonly extraBody?: Record<string, unknown>
  /**
   * Extended thinking / reasoning configuration. Provider mapping:
   * - Anthropic: forwards `enabled` + `budgetTokens` as the `thinking` request
   *   param (see `toAnthropicThinkingParam`).
   * - Gemini: forwards `enabled` + `budgetTokens` as `thinkingConfig`
   *   (`includeThoughts` defaults on when enabled).
   * - OpenAI: forwards `effort` as `reasoning_effort` (o-series, gpt-5).
   *   The `enabled`/`budgetTokens` fields are ignored — OpenAI's reasoning
   *   surface is qualitative (`effort`), not quantitative.
   * - All other providers (Bedrock, local servers, etc.): ignored. Use
   *   {@link extraBody} for provider-specific reasoning controls instead.
   */
  readonly thinking?: ThinkingConfig
  /**
   * Maximum wall-clock time (in milliseconds) for the entire agent run.
   * When exceeded, the run is aborted via `AbortSignal.timeout()`.
   * Useful for local models where inference can be unpredictably slow.
   */
  readonly timeoutMs?: number
  /**
   * Loop detection configuration. When set, the agent tracks repeated tool
   * calls and text outputs to detect stuck loops before `maxTurns` is reached.
   */
  readonly loopDetection?: LoopDetectionConfig
  /**
   * Maximum tool output length in characters for all tools used by this agent.
   * When set, tool outputs exceeding this limit are truncated (head + tail
   * with a marker in between). Per-tool {@link ToolDefinition.maxOutputChars}
   * takes priority over this value.
   */
  readonly maxToolOutputChars?: number
  /**
   * Compress tool results that the agent has already processed.
   *
   * In multi-turn runs, tool results persist in the conversation even after the
   * agent has acted on them. When enabled, consumed tool results (those followed
   * by an assistant response) are replaced with a short marker before the next
   * LLM call, freeing context budget for new reasoning.
   *
   * - `true` — enable with default threshold (500 chars)
   * - `{ minChars: N }` — only compress results longer than N characters
   * - `false` / `undefined` — disabled (default)
   *
   * Error tool results are never compressed.
   */
  readonly compressToolResults?: boolean | { readonly minChars?: number }
  /**
   * Opt-in: when an outbound IR-to-native conversion encounters a
   * {@link ReasoningBlock} that the target adapter cannot natively echo
   * (see {@link LLMAdapter.capabilities.echoesReasoning}), downgrade the
   * block to `<thinking>...</thinking>` text and prepend to the next text
   * part instead of dropping it silently (today's default).
   *
   * Triggers in two scenarios:
   *  1. **Same-provider replay** through a `'never'` adapter (e.g. an
   *     OpenAI o-series response carries `reasoning_content` that gets
   *     extracted to a `ReasoningBlock`; on the next turn it would
   *     normally be dropped because Chat Completions doesn't accept
   *     reasoning input).
   *  2. **Cross-provider handoff** — `Agent.prompt()` switching `model`
   *     mid-conversation, or any other path that puts a block from one
   *     provider in front of another adapter.
   *
   * Default `false` because enabling silently inflates prompt tokens
   * (reasoning blocks can be very long) and emits the model's internal
   * thinking text to potentially different providers, both of which the
   * user should consent to. See #223 for the design discussion.
   *
   * Caveat: some OpenAI-compatible local models echo `<thinking>` text
   * back as if it were instruction, which can trip the loop detector —
   * see `examples/patterns/cross-provider-reasoning.ts`.
   */
  readonly preserveReasoningAsText?: boolean
  /**
   * Truncate the inner text of each `<thinking>` block emitted by the
   * {@link preserveReasoningAsText} fallback. Shares the
   * `boolean | { minChars?: number }` shape with
   * {@link compressToolResults}, but **the compression method differs**:
   *
   *   - `compressToolResults`: blocks over the threshold are *replaced*
   *     wholesale with a short marker string.
   *   - `compressReasoningText`: blocks over the threshold are kept but
   *     reduced to a head+tail excerpt that fits within `minChars`. The
   *     same number serves as both the activation threshold AND the cap on
   *     the post-truncation length.
   *
   * Values:
   * - `true` — enable with default 1200-char head+tail excerpt.
   * - `{ minChars: N }` — pass blocks of length ≤ N untouched; truncate
   *   longer blocks to fit within N chars.
   * - `false` — never truncate (footgun for long CoT, not recommended).
   * - `undefined` — defaults to `true` when {@link preserveReasoningAsText}
   *   is `true`, else `false`. Matches the maintainer's guidance in #223
   *   to "tie compress default-on to the preserve flag".
   *
   * Has no effect when {@link preserveReasoningAsText} is `false` (no
   * fallback runs, so nothing to truncate).
   */
  readonly compressReasoningText?: boolean | { readonly minChars?: number }
  /**
   * Optional Zod schema for structured output.  When set, the agent's final
   * output is parsed as JSON and validated against this schema.  A single
   * retry with error feedback is attempted on validation failure.
   *
   * **Distinct from {@link ToolDefinition.outputSchema}**, which validates an
   * individual tool's `ToolResult.data` string. This one operates on the
   * agent's final answer as parsed JSON.
   */
  readonly outputSchema?: ZodSchema
  /**
   * Called before each agent run. Receives the prompt and agent config.
   * Return a (possibly modified) context to continue, or throw to abort the run.
   * Only `prompt` from the returned context is applied; `agent` is read-only informational.
   */
  readonly beforeRun?: (context: BeforeRunHookContext) => Promise<BeforeRunHookContext> | BeforeRunHookContext
  /**
   * Called after each agent run completes successfully. Receives the run result.
   * Return a (possibly modified) result, or throw to mark the run as failed.
   * Not called when the run throws. For error observation, handle errors at the call site.
   */
  readonly afterRun?: (result: AgentRunResult) => Promise<AgentRunResult> | AgentRunResult
}

// ---------------------------------------------------------------------------
// Loop detection
// ---------------------------------------------------------------------------

/** Configuration for agent loop detection. */
export interface LoopDetectionConfig {
  /**
   * Maximum consecutive times the same tool call (name + args) or text
   * output can repeat before detection triggers. Default: `3`.
   */
  readonly maxRepetitions?: number
  /**
   * Number of recent turns to track for repetition analysis. Default: `4`.
   */
  readonly loopDetectionWindow?: number
  /**
   * Action to take when a loop is detected.
   * - `'warn'`      — inject a "you appear stuck" message, give the LLM one
   *                    more chance; terminate if the loop persists (default)
   * - `'terminate'` — stop the run immediately
   * - `function`    — custom callback (sync or async); return `'continue'`,
   *                    `'inject'`, or `'terminate'` to control the outcome
   */
  readonly onLoopDetected?: 'warn' | 'terminate' | ((info: LoopDetectionInfo) => 'continue' | 'inject' | 'terminate' | Promise<'continue' | 'inject' | 'terminate'>)
}

/** Diagnostic payload emitted when a loop is detected. */
export interface LoopDetectionInfo {
  readonly kind: 'tool_repetition' | 'text_repetition'
  /** Number of consecutive identical occurrences observed. */
  readonly repetitions: number
  /** Human-readable description of the detected loop. */
  readonly detail: string
}

/** Lifecycle state tracked during an agent run. */
export interface AgentState {
  status: 'idle' | 'running' | 'completed' | 'error'
  messages: LLMMessage[]
  tokenUsage: TokenUsage
  error?: Error
}

/** A single recorded tool invocation within a run. */
export interface ToolCallRecord {
  readonly toolName: string
  readonly input: Record<string, unknown>
  readonly output: string
  /** Wall-clock duration in milliseconds. */
  readonly duration: number
}

/** The final result produced when an agent run completes (or fails). */
export interface AgentRunResult {
  readonly success: boolean
  readonly output: string
  readonly messages: LLMMessage[]
  readonly tokenUsage: TokenUsage
  readonly toolCalls: ToolCallRecord[]
  /**
   * Parsed and validated structured output when `outputSchema` is set on the
   * agent config.  `undefined` when no schema is configured or validation
   * failed after retry.
   */
  readonly structured?: unknown
  /** True when the run was terminated or warned due to loop detection. */
  readonly loopDetected?: boolean
  /** True when the run stopped because token budget was exceeded. */
  readonly budgetExceeded?: boolean
}

// ---------------------------------------------------------------------------
// Team
// ---------------------------------------------------------------------------

/** Static configuration for a team of cooperating agents. */
export interface TeamConfig {
  readonly name: string
  readonly agents: readonly AgentConfig[]
  readonly sharedMemory?: boolean
  /**
   * Custom {@link MemoryStore} backing the team's shared memory (e.g. Redis,
   * Postgres, or a remote service). When provided, shared memory is enabled
   * regardless of `sharedMemory`. When both are set, `sharedMemoryStore` wins.
   * When omitted and `sharedMemory` is `true`, the default in-memory store is used.
   *
   * SDK-only: the CLI (`oma`) cannot pass runtime objects through its JSON config.
   */
  readonly sharedMemoryStore?: MemoryStore
  readonly maxConcurrency?: number
}

/**
 * Per-call options for {@link OpenMultiAgent.runTeam}. Differs from
 * {@link OrchestratorConfig} by being scoped to a single invocation.
 */
export interface RunTeamOptions {
  readonly abortSignal?: AbortSignal
  readonly coordinator?: CoordinatorConfig
  /**
   * When true, the coordinator decomposes the goal but no task agents run.
   * The returned {@link TeamRunResult} has `planOnly: true`, `success: true`,
   * `tasks` populated (all `pending`, no metrics), and `agentResults`
   * containing only the coordinator's decomposition call. `totalTokenUsage`
   * reflects the coordinator only.
   *
   * Bypasses the simple-goal short-circuit so the coordinator always runs.
   *
   * If {@link OrchestratorConfig.onPlanReady} is wired up and returns false,
   * the rejection wins: result is `success: false` and `planOnly` is undefined.
   */
  readonly planOnly?: boolean
  /**
   * When true, prepends a team-context block (original goal, full roster, and
   * this worker's assignee identity) to every worker prompt under `runTeam`.
   * Helps workers stay aligned with the overall goal and makes multi-step runs
   * easier to debug.
   *
   * Default: `false` — worker prompts are byte-identical to today.
   *
   * Scope: `runTeam` only. `runTasks` (no goal concept) and the short-circuit
   * single-agent path (no coordinator) ignore this option.
   */
  readonly revealCoordinator?: boolean
}

/** Aggregated result for a full team run. */
export interface TeamRunResult {
  readonly success: boolean
  readonly goal?: string
  readonly tasks?: readonly TaskExecutionRecord[]
  /**
   * True when the run was a plan-only invocation (`runTeam(team, goal, { planOnly: true })`).
   * The coordinator decomposed the goal but no task agents executed.
   * Undefined on normal runs and on `onPlanReady`-rejection results.
   */
  readonly planOnly?: boolean
  /** Keyed by agent name. */
  readonly agentResults: Map<string, AgentRunResult>
  readonly totalTokenUsage: TokenUsage
}

/** A single serializable task in a deterministic replay plan. */
export interface PlanTaskArtifact {
  readonly id: string
  readonly title: string
  readonly description: string
  readonly assignee?: string
  readonly dependsOn?: readonly string[]
  readonly memoryScope?: 'dependencies' | 'all'
  readonly maxRetries?: number
  readonly retryDelayMs?: number
  readonly retryBackoff?: number
}

/**
 * Serializable plan artifact that can be persisted and later replayed without
 * invoking the coordinator again.
 */
export interface PlanArtifact {
  readonly version: 1
  readonly goal?: string
  readonly tasks: readonly PlanTaskArtifact[]
}

// ---------------------------------------------------------------------------
// Task
// ---------------------------------------------------------------------------

/** Valid states for a {@link Task}. */
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked' | 'skipped'

/**
 * Metrics shown in the team-run dashboard detail panel for a single task.
 * Mirrors execution data collected during orchestration.
 */
export interface TaskExecutionMetrics {
  readonly startMs: number
  readonly endMs: number
  readonly durationMs: number
  readonly tokenUsage: TokenUsage
  readonly toolCalls: AgentRunResult['toolCalls']
}

/** Serializable task snapshot embedded in the static HTML dashboard. */
export interface TaskExecutionRecord {
  readonly id: string
  readonly title: string
  readonly assignee?: string
  readonly status: TaskStatus
  readonly dependsOn: readonly string[]
  readonly description?: string
  /**
   * Execution config, carried so a `planOnly` snapshot can be serialized into a
   * lossless replay artifact (see {@link PlanTaskArtifact}). Populated from the
   * task; `undefined` when the task did not set them.
   */
  readonly memoryScope?: 'dependencies' | 'all'
  readonly maxRetries?: number
  readonly retryDelayMs?: number
  readonly retryBackoff?: number
  readonly metrics?: TaskExecutionMetrics
}

/** A discrete unit of work tracked by the orchestrator. */
export interface Task {
  readonly id: string
  readonly title: string
  readonly description: string
  status: TaskStatus
  /** Agent name responsible for executing this task. */
  assignee?: string
  /** IDs of tasks that must complete before this one can start. */
  dependsOn?: readonly string[]
  /**
   * Controls what prior team context is injected into this task's prompt.
   * - `dependencies` (default): only direct dependency task results
   * - `all`: full shared-memory summary
   */
  readonly memoryScope?: 'dependencies' | 'all'
  result?: string
  readonly createdAt: Date
  updatedAt: Date
  /** Maximum number of retry attempts on failure (default: 0 — no retry). */
  readonly maxRetries?: number
  /** Base delay in ms before the first retry (default: 1000). */
  readonly retryDelayMs?: number
  /** Exponential backoff multiplier (default: 2). */
  readonly retryBackoff?: number
}

// ---------------------------------------------------------------------------
// Orchestrator
// ---------------------------------------------------------------------------

/**
 * Progress event emitted by the orchestrator during a run.
 *
 * **v0.3 addition:** `'task_skipped'` — consumers with exhaustive switches
 * on `type` will need to add a case for this variant.
 */
export interface OrchestratorEvent {
  readonly type:
    | 'agent_start'
    | 'agent_complete'
    | 'task_start'
    | 'task_complete'
    | 'task_skipped'
    | 'task_retry'
    | 'budget_exceeded'
    | 'message'
    | 'error'
  readonly agent?: string
  readonly task?: string
  readonly data?: unknown
}

/** Top-level configuration for the orchestrator. */
export interface OrchestratorConfig {
  readonly maxConcurrency?: number
  /**
   * Maximum depth of `delegate_to_agent` chains from a task run (default `3`).
   * Depth is per nested delegated run, not per team.
   */
  readonly maxDelegationDepth?: number
  /** Maximum cumulative tokens (input + output) allowed per orchestrator run. */
  readonly maxTokenBudget?: number
  readonly defaultModel?: string
  readonly defaultProvider?: SupportedProvider
  readonly defaultBaseURL?: string
  readonly defaultApiKey?: string
  /**
   * Default root directory for built-in filesystem tools when an agent does
   * not set its own {@link AgentConfig.cwd}. Defaults to
   * `<process.cwd()>/.agent-workspace`, a sandbox subdirectory auto-created
   * on first write. Pass `process.cwd()` to widen the sandbox to the entire
   * working directory, or `null` to disable it globally (filesystem tools
   * then accept arbitrary absolute or relative paths).
   */
  readonly defaultCwd?: string | null
  readonly onProgress?: (event: OrchestratorEvent) => void
  readonly onTrace?: (event: TraceEvent) => void | Promise<void>
  /**
   * Optional approval gate called between task execution rounds.
   *
   * After a batch of tasks completes, this callback receives all
   * completed {@link Task}s from that round and the list of tasks about
   * to start next. Return `true` to continue or `false` to abort —
   * remaining tasks will be marked `'skipped'`.
   *
   * Not called when:
   * - No tasks succeeded in the round (all failed).
   * - No pending tasks remain after the round (final batch).
   *
   * **Note:** Do not mutate the {@link Task} objects passed to this
   * callback — they are live references to queue state. Mutation is
   * undefined behavior.
   */
  readonly onApproval?: (completedTasks: readonly Task[], nextTasks: readonly Task[]) => Promise<boolean>
  /**
   * Optional approval gate called once after the coordinator decomposes the
   * goal into tasks and before execution begins.
   *
   * Receives the full plan as a {@link Task} array. Return `true` to proceed
   * or `false` to abort. A thrown callback is treated as an abort.
   *
   * Only invoked by `runTeam()`. `runAgent()` and `runTasks()` are
   * unaffected. The `TeamRunResult` returned on abort still reflects the
   * coordinator's decomposition tokens.
   *
   * **Note:** Do not mutate the {@link Task} objects passed to this
   * callback. They are live references to queue state; mutation is
   * undefined behavior.
   */
  readonly onPlanReady?: (tasks: readonly Task[]) => Promise<boolean>
  /**
   * Called for each streaming event emitted by an agent during runTeam().
   * When provided, agents run in streaming mode so the TUI can receive
   * real-time text deltas and tool-call events.
   * Only invoked by runTeam(). Not called for runAgent() or runTasks().
   */
  readonly onAgentStream?: (agentName: string, event: StreamEvent) => void
}

/**
 * Optional overrides for the temporary coordinator agent created by `runTeam`.
 *
 * All fields are optional. Unset fields fall back to orchestrator defaults
 * (or coordinator built-in defaults where applicable).
 */
export interface CoordinatorConfig {
  /** Coordinator model. Defaults to `OrchestratorConfig.defaultModel`. */
  readonly model?: string
  /**
   * Optional {@link LLMAdapter} for the coordinator agent. When set, coordinator
   * LLM calls use this adapter and ignore `provider` / `apiKey` / `baseURL`.
   */
  readonly adapter?: LLMAdapter
  readonly provider?: SupportedProvider
  readonly baseURL?: string
  readonly apiKey?: string
  /**
   * Full system prompt override. When set, this replaces the default
   * coordinator preamble and decomposition guidance.
   *
   * Team roster, output format, and synthesis sections are still appended.
   */
  readonly systemPrompt?: string
  /**
   * Additional instructions appended to the default coordinator prompt.
   * Ignored when `systemPrompt` is provided.
   */
  readonly instructions?: string
  readonly maxTurns?: number
  readonly maxTokens?: number
  readonly temperature?: number
  /** See {@link AgentConfig.frequencyPenalty}. */
  readonly frequencyPenalty?: number
  /** See {@link AgentConfig.presencePenalty}. */
  readonly presencePenalty?: number
  /** See {@link AgentConfig.topP}. */
  readonly topP?: number
  /** See {@link AgentConfig.topK}. */
  readonly topK?: number
  /** See {@link AgentConfig.minP}. */
  readonly minP?: number
  /** See {@link AgentConfig.parallelToolCalls}. */
  readonly parallelToolCalls?: boolean
  /**
   * Adapter-specific escape hatch merged into the outgoing request payload.
   * Spread between the sampling params and the structural fields, so values
   * here can override the standard sampling defaults (`temperature`, `topP`,
   * etc.) but cannot override transport-level fields (`model`, `messages`,
   * `tools`, `stream`, and Anthropic's `system`).
   */
  readonly extraBody?: Record<string, unknown>
  /** Predefined tool preset for common coordinator use cases. */
  readonly toolPreset?: 'readonly' | 'readwrite' | 'full'
  /** Tool names available to the coordinator. */
  readonly tools?: readonly string[]
  /** Tool names explicitly denied to the coordinator. */
  readonly disallowedTools?: readonly string[]
  /**
   * Root directory used by the coordinator's filesystem tools.
   * Defaults to {@link OrchestratorConfig.defaultCwd}. Pass `null` to
   * disable the sandbox for the coordinator only.
   */
  readonly cwd?: string | null
  readonly loopDetection?: LoopDetectionConfig
  readonly timeoutMs?: number
}

// ---------------------------------------------------------------------------
// Trace events — lightweight observability spans
// ---------------------------------------------------------------------------

/** Trace event type discriminants. */
export type TraceEventType =
  | 'llm_call'
  | 'tool_call'
  | 'task'
  | 'agent'
  | 'plan_ready'
  | 'agent_stream'

/** Shared fields present on every trace event. */
export interface TraceEventBase {
  /** Unique identifier for the entire run (runTeam / runTasks / runAgent call). */
  readonly runId: string
  readonly type: TraceEventType
  /** Unix epoch ms when the span started. */
  readonly startMs: number
  /** Unix epoch ms when the span ended. */
  readonly endMs: number
  /** Wall-clock duration in milliseconds (`endMs - startMs`). */
  readonly durationMs: number
  /** Agent name associated with this span. */
  readonly agent: string
  /** Task ID associated with this span. */
  readonly taskId?: string
}

/** Emitted for each LLM API call (one per agent turn). */
export interface LLMCallTrace extends TraceEventBase {
  readonly type: 'llm_call'
  readonly model: string
  /** Distinguishes normal turn calls from context-summary calls. */
  readonly phase?: 'turn' | 'summary'
  readonly turn: number
  readonly tokens: TokenUsage
}

/** Emitted for each tool execution. */
export interface ToolCallTrace extends TraceEventBase {
  readonly type: 'tool_call'
  readonly tool: string
  readonly isError: boolean
  /** The input arguments passed to the tool after best-effort sensitive-field redaction. */
  readonly input: Record<string, unknown>
  /** The serialised output returned by the tool after executor truncation and best-effort sensitive-value redaction. */
  readonly output: string
}

/** Emitted when a task completes (wraps the full retry sequence). */
export interface TaskTrace extends TraceEventBase {
  readonly type: 'task'
  readonly taskId: string
  readonly taskTitle: string
  readonly success: boolean
  readonly retries: number
}

/** Emitted when an agent run completes (wraps the full conversation loop). */
export interface AgentTrace extends TraceEventBase {
  readonly type: 'agent'
  readonly turns: number
  readonly tokens: TokenUsage
  readonly toolCalls: number
}

/** Emitted when runTeam reaches the plan approval boundary. */
export interface PlanReadyTrace extends TraceEventBase {
  readonly type: 'plan_ready'
  /** Number of tasks produced by coordinator decomposition. */
  readonly taskCount: number
  /** Approval decision returned by onPlanReady callback. */
  readonly approved: boolean
}

/** Emitted for each streaming event forwarded through onAgentStream in runTeam. */
export interface AgentStreamTrace extends TraceEventBase {
  readonly type: 'agent_stream'
  /** Underlying stream event type (`text`, `tool_use`, `done`, etc.). */
  readonly streamType: StreamEvent['type']
}

/** Discriminated union of all trace event types. */
export type TraceEvent =
  | LLMCallTrace
  | ToolCallTrace
  | TaskTrace
  | AgentTrace
  | PlanReadyTrace
  | AgentStreamTrace

// ---------------------------------------------------------------------------
// Memory
// ---------------------------------------------------------------------------

/** JSON-serializable value accepted by {@link SharedMemory}. */
export type SharedMemoryValue =
  | string
  | number
  | boolean
  | null
  | readonly SharedMemoryValue[]
  | { readonly [key: string]: SharedMemoryValue }

/** Parsed entry returned by {@link SharedMemory}; the raw {@link MemoryStore} remains string-only. */
export interface SharedMemoryEntry extends Omit<MemoryEntry, 'value'> {
  readonly value: SharedMemoryValue
}

/** Optional write-time validation for shared memory values. */
export interface SharedMemoryWriteOptions {
  readonly schema?: ZodSchema<SharedMemoryValue>
}
/** A single key-value record stored in a {@link MemoryStore}. */
export interface MemoryEntry {
  readonly key: string
  readonly value: string
  readonly metadata?: Readonly<Record<string, unknown>>
  readonly createdAt: Date
  /**
   * Optional turn-count expiry. When set, the entry is considered expired
   * once the {@link SharedMemory} turn counter reaches or exceeds this value.
   * Computed at write time as `currentTurn + ttlTurns`. Stores that don't
   * implement {@link MemoryStore.setWithExpiry} will not have this field set.
   */
  readonly expiresAtTurn?: number
}

/**
 * Persistent (or in-memory) key-value store shared across agents.
 * Implementations may be backed by Redis, SQLite, or plain objects.
 */
export interface MemoryStore {
  get(key: string): Promise<MemoryEntry | null>
  set(key: string, value: string, metadata?: Record<string, unknown>): Promise<void>
  /**
   * Optional: write an entry with a turn-count expiry. Stores that don't
   * implement this method silently lose TTL semantics — callers (e.g.
   * {@link SharedMemory}) should fall back to {@link set} and not enforce
   * expiry on entries from those backends.
   */
  setWithExpiry?(
    key: string,
    value: string,
    expiresAtTurn: number,
    metadata?: Record<string, unknown>,
  ): Promise<void>
  list(): Promise<MemoryEntry[]>
  delete(key: string): Promise<void>
  clear(): Promise<void>
}

// ---------------------------------------------------------------------------
// LLM adapter
// ---------------------------------------------------------------------------

/** Options shared by both chat and streaming calls. */
export interface LLMChatOptions {
  readonly model: string
  readonly tools?: readonly LLMToolDef[]
  readonly maxTokens?: number
  readonly temperature?: number
  /** See {@link AgentConfig.frequencyPenalty}. */
  readonly frequencyPenalty?: number
  /** See {@link AgentConfig.presencePenalty}. */
  readonly presencePenalty?: number
  /** See {@link AgentConfig.topP}. */
  readonly topP?: number
  /** See {@link AgentConfig.topK}. */
  readonly topK?: number
  /** See {@link AgentConfig.minP}. */
  readonly minP?: number
  /** See {@link AgentConfig.parallelToolCalls}. */
  readonly parallelToolCalls?: boolean
  /**
   * Adapter-specific escape hatch merged into the outgoing request payload.
   * Spread between the sampling params and the structural fields, so values
   * here can override the standard sampling defaults (`temperature`, `topP`,
   * etc.) but cannot override transport-level fields (`model`, `messages`,
   * `tools`, `stream`, and Anthropic's `system`).
   */
  readonly extraBody?: Record<string, unknown>
  /** See {@link AgentConfig.thinking}. */
  readonly thinking?: ThinkingConfig
  /** See {@link AgentConfig.preserveReasoningAsText}. */
  readonly preserveReasoningAsText?: boolean
  /** See {@link AgentConfig.compressReasoningText}. */
  readonly compressReasoningText?: boolean | { readonly minChars?: number }
  readonly systemPrompt?: string
  readonly abortSignal?: AbortSignal
}

/**
 * Options for streaming calls.
 * Extends {@link LLMChatOptions} without additional fields — the separation
 * exists so callers can type-narrow and implementations can diverge later.
 */
export interface LLMStreamOptions extends LLMChatOptions {}

/**
 * Provider-agnostic interface that every LLM backend must implement.
 *
 * @example
 * ```ts
 * const adapter: LLMAdapter = createAdapter('anthropic')
 * const response = await adapter.chat(messages, { model: 'claude-opus-4-6' })
 * ```
 */
export interface LLMAdapter {
  /** Human-readable provider name, e.g. `'anthropic'` or `'openai'`. */
  readonly name: string

  /**
   * Send a chat request and return the complete response.
   * Throws on non-retryable API errors.
   */
  chat(messages: LLMMessage[], options: LLMChatOptions): Promise<LLMResponse>

  /**
   * Send a chat request and yield {@link StreamEvent}s incrementally.
   * The final event in the sequence always has `type === 'done'` on success,
   * or `type === 'error'` on failure.
   */
  stream(messages: LLMMessage[], options: LLMStreamOptions): AsyncIterable<StreamEvent>

  /**
   * Optional declaration of how this adapter handles {@link ReasoningBlock}
   * round-tripping. Consumed by the cross-provider reasoning fallback layer
   * (see #223) to decide whether to native-echo an incoming `ReasoningBlock`
   * or downgrade it to `<thinking>` text on outbound conversion.
   *
   * - `'never'`: This adapter's wire protocol does not accept reasoning input
   *   under any circumstance (e.g. OpenAI Chat Completions). Outbound paths
   *   must fall back regardless of {@link ReasoningBlock.provenance}.
   * - `'own-issued'`: Native echo is only ever attempted when
   *   `block.provenance === this.name`. Provenance match is **necessary
   *   but not sufficient** — adapters MAY impose additional preconditions
   *   (e.g. Anthropic requires the original `signature`; Gemini drops
   *   unsigned thought summaries to keep context lean). Phase 2
   *   implementers MUST consult the adapter-specific outbound serializer
   *   for the full eligibility rule, not just the provenance match.
   * - `'tool-use-only'`: Native echo is attempted on every assistant
   *   message inside a tool-calling conversation (one that contains at
   *   least one `tool_use` block anywhere in its history), gated by
   *   `block.provenance === this.name`. This includes the final synthesis
   *   message that has no `tool_use` of its own — omitting it would 400
   *   the next user turn. Conversations with no `tool_use` anywhere skip
   *   the echo entirely (per spec, reasoning is ignored there but would
   *   still bloat context). Used by DeepSeek V4 in thinking mode.
   *
   * Omitted on third-party adapters that pre-date this field; callers treat
   * `undefined` as `'never'` to preserve today's silent-drop behaviour.
   */
  readonly capabilities?: {
    readonly echoesReasoning: 'never' | 'own-issued' | 'tool-use-only'
  }
}
