Skip to content

musician

musician

Baton musician — single-attempt sheet execution.

The musician is the bridge between the baton's dispatch decision and the backend's execution. It plays ONE attempt, runs validations, records outcomes, and reports back via SheetAttemptResult into the baton's inbox.

The musician NEVER retries. It NEVER decides completion mode, escalation, or rate limit recovery. It reports; the conductor (baton) decides.

The 6-step flow
  1. Build prompt (render template + inject context)
  2. Configure backend (timeout, working directory)
  3. Play: backend.execute(prompt, timeout_seconds=sheet.timeout)
  4. Listen: run validations on output (if execution succeeded)
  5. Record: capture output with credential redaction
  6. Report: put SheetAttemptResult into baton inbox
Design decisions
  • The musician is a free function, not a class. No state to manage.
  • Exceptions from the backend are caught and reported, never re-raised. The baton must never crash because a backend threw.
  • Credential redaction happens before the result is put in the inbox. No unscanned output ever reaches the baton.
  • F-018 contract: when execution succeeds with no validations, validation_pass_rate is set to 100.0. The default 0.0 would cause unnecessary retries.
  • F-104: Full Jinja2 prompt rendering with preamble, variable expansion, prelude/cadenza injection, and validation requirements. The musician renders the template at execution time because cross-sheet context only exists after earlier sheets complete.

See: docs/plans/2026-03-26-baton-design.md — Single-Attempt Musician

Attributes

Classes

Functions

sheet_task async

sheet_task(*, job_id, sheet, backend, attempt_context, inbox, total_sheets=1, total_movements=1, rendered_prompt=None, preamble=None, cost_per_1k_input=None, cost_per_1k_output=None, instrument_override=None)

Execute a single sheet attempt and report the result.

This is the musician's entire job. Play once, report in full detail. The conductor (baton) decides what happens next.

Parameters:

Name Type Description Default
job_id str

The job this sheet belongs to.

required
sheet Sheet

The sheet to execute (prompt, validations, timeout, etc.).

required
backend Backend

The backend to execute through.

required
attempt_context AttemptContext

Context from the conductor (attempt number, mode, etc.).

required
inbox Queue[SheetAttemptResult]

The baton's event inbox to report results to.

required
total_sheets int

Total concrete sheets in the job (for template variables).

1
total_movements int

Total movements in the job (for template variables).

1
rendered_prompt str | None

Optional pre-rendered prompt from PromptRenderer. When provided, the musician uses this directly instead of calling _build_prompt(). This enables the full 9-layer prompt assembly pipeline including spec fragments, learned patterns, and failure history.

None
preamble str | None

Optional pre-built preamble. Set on the backend via set_preamble() before execution. Only used when rendered_prompt is also provided (the PromptRenderer separates them).

None
cost_per_1k_input float | None

Cost per 1000 input tokens (USD) from the instrument profile's ModelCapacity. None uses hardcoded fallback.

None
cost_per_1k_output float | None

Cost per 1000 output tokens (USD) from the instrument profile's ModelCapacity. None uses hardcoded fallback.

None
instrument_override str | None

When set, used as the instrument_name in the SheetAttemptResult instead of sheet.instrument_name. Required after instrument fallback — the Sheet entity keeps the original instrument but the baton's SheetExecutionState tracks the fallback instrument. Without this, attempt results credit the wrong instrument for success/failure tracking.

None

Never raises — all exceptions are caught and reported via the inbox.

Source code in src/marianne/daemon/baton/musician.py
async def sheet_task(
    *,
    job_id: str,
    sheet: Sheet,
    backend: Backend,
    attempt_context: AttemptContext,
    inbox: asyncio.Queue[SheetAttemptResult],
    total_sheets: int = 1,
    total_movements: int = 1,
    rendered_prompt: str | None = None,
    preamble: str | None = None,
    cost_per_1k_input: float | None = None,
    cost_per_1k_output: float | None = None,
    instrument_override: str | None = None,
) -> None:
    """Execute a single sheet attempt and report the result.

    This is the musician's entire job. Play once, report in full detail.
    The conductor (baton) decides what happens next.

    Args:
        job_id: The job this sheet belongs to.
        sheet: The sheet to execute (prompt, validations, timeout, etc.).
        backend: The backend to execute through.
        attempt_context: Context from the conductor (attempt number, mode, etc.).
        inbox: The baton's event inbox to report results to.
        total_sheets: Total concrete sheets in the job (for template variables).
        total_movements: Total movements in the job (for template variables).
        rendered_prompt: Optional pre-rendered prompt from PromptRenderer.
            When provided, the musician uses this directly instead of
            calling _build_prompt(). This enables the full 9-layer
            prompt assembly pipeline including spec fragments, learned
            patterns, and failure history.
        preamble: Optional pre-built preamble. Set on the backend via
            set_preamble() before execution. Only used when rendered_prompt
            is also provided (the PromptRenderer separates them).
        cost_per_1k_input: Cost per 1000 input tokens (USD) from the
            instrument profile's ModelCapacity. None uses hardcoded fallback.
        cost_per_1k_output: Cost per 1000 output tokens (USD) from the
            instrument profile's ModelCapacity. None uses hardcoded fallback.
        instrument_override: When set, used as the instrument_name in the
            SheetAttemptResult instead of sheet.instrument_name. Required
            after instrument fallback — the Sheet entity keeps the original
            instrument but the baton's SheetExecutionState tracks the
            fallback instrument. Without this, attempt results credit
            the wrong instrument for success/failure tracking.

    Never raises — all exceptions are caught and reported via the inbox.
    """
    start_time = time.monotonic()
    # Resolve the effective instrument name — fallback may have changed it
    effective_instrument = instrument_override or sheet.instrument_name

    try:
        # Step 1: Build prompt
        if rendered_prompt is not None:
            # F-104 via PromptRenderer — pre-rendered with all 9 layers.
            # Preamble is separated and set on the backend directly.
            prompt = rendered_prompt
            if preamble is not None:
                backend.set_preamble(preamble)
        else:
            # Fallback: inline rendering (covers basic cases)
            prompt = _build_prompt(
                sheet, attempt_context,
                total_sheets=total_sheets,
                total_movements=total_movements,
            )

        # Step 2-3: Execute through backend
        exec_result = await _execute(backend, prompt, sheet.timeout_seconds)

        # Step 4: Run validations (only if execution succeeded)
        # F-118: pass total_sheets/total_movements for rich context
        val_passed, val_total, val_rate, val_details = await _validate(
            sheet, exec_result,
            total_sheets=total_sheets,
            total_movements=total_movements,
        )

        # Step 5: Record output with credential redaction
        stdout_tail, stderr_tail = _capture_output(exec_result)

        # Step 6: Classify errors (redact credentials from error messages —
        # backend error_message can contain API keys from auth failures,
        # config errors, or URL parameters)
        error_class, raw_error_msg = _classify_error(exec_result)
        error_msg = redact_credentials(raw_error_msg) if raw_error_msg else raw_error_msg

        # Build and report result
        duration = exec_result.duration_seconds
        result = SheetAttemptResult(
            job_id=job_id,
            sheet_num=sheet.num,
            instrument_name=effective_instrument,
            attempt=attempt_context.attempt_number,
            execution_success=exec_result.success,
            exit_code=exec_result.exit_code,
            duration_seconds=duration,
            validations_passed=val_passed,
            validations_total=val_total,
            validation_pass_rate=val_rate,
            validation_details=val_details,
            error_classification=error_class,
            error_message=error_msg,
            rate_limited=exec_result.rate_limited,
            rate_limit_wait_seconds=exec_result.rate_limit_wait_seconds,
            cost_usd=_estimate_cost(
                exec_result,
                cost_per_1k_input=cost_per_1k_input,
                cost_per_1k_output=cost_per_1k_output,
            ),
            input_tokens=exec_result.input_tokens or 0,
            output_tokens=exec_result.output_tokens or 0,
            model_used=exec_result.model,
            stdout_tail=stdout_tail,
            stderr_tail=stderr_tail,
        )

    except Exception as exc:
        # Step 6 (exception path): Never crash the baton
        duration = time.monotonic() - start_time
        raw_error_msg = f"{type(exc).__name__}: {exc}"
        # Redact credentials from exception messages before logging/storing.
        # Exception text can contain API keys (e.g., auth failures that echo
        # the key, config loading errors with key values in paths). Without
        # redaction, credentials propagate to logs, state DB, dashboard,
        # learning store, and diagnostic output — 6+ storage locations.
        error_msg = redact_credentials(raw_error_msg) or raw_error_msg
        _logger.error(
            "musician.sheet_task.exception",
            extra={
                "job_id": job_id,
                SHEET_NUM_KEY: sheet.num,
                "error": error_msg,
            },
            exc_info=True,
        )

        result = SheetAttemptResult(
            job_id=job_id,
            sheet_num=sheet.num,
            instrument_name=effective_instrument,
            attempt=attempt_context.attempt_number,
            execution_success=False,
            exit_code=None,
            duration_seconds=duration,
            error_classification="TRANSIENT",
            error_message=error_msg,
            rate_limited=False,
        )

    # Always report — the baton must know what happened
    await inbox.put(result)
    _logger.info(
        "musician.sheet_task.reported",
        extra={
            "job_id": job_id,
            SHEET_NUM_KEY: sheet.num,
            "success": result.execution_success,
            "pass_rate": result.validation_pass_rate,
            "duration": result.duration_seconds,
        },
    )