runSession method

Future<SessionTermination> runSession()

Run a full session. Returns when one of the PRD §17 termination conditions fires:

  • 50 turns OR 15 minutes wall-clock → budget_exhausted
  • 3 consecutive failed turns → harness_error agent_stuck
  • VM connection lost mid-turn → harness_error connection_lost
  • malformed observation envelope → harness_error observation_envelope_rejected
  • any unclassified escaping exception → harness_error unclassified (the footer names it; the exception still propagates)
  • voluntary core.done(reason)done

On termination the trajectory writer is closed with the final footer (idempotently). BindingNotInitializedError is raised by LeonardSession.start() before runSession is invoked, so it is never observed here.

Implementation

Future<SessionTermination> runSession() async {
  _sessionStart = _clock();
  SessionTermination? termination;
  Object? escaped;
  try {
    while (true) {
      if (_turnIndex >= _maxTurns ||
          _clock().difference(_sessionStart!) >= _sessionBudget) {
        termination = SessionTermination(SessionOutcome.budgetExhausted);
        return termination;
      }
      if (_consecutiveFailedTurns >= _kMaxConsecutiveFailedTurns) {
        termination = SessionTermination(
          SessionOutcome.harnessError,
          harnessError: HarnessError.agentStuck,
          terminationDetail: _lastFailureDetail,
        );
        return termination;
      }
      if (_consecutiveTurnTimeouts >= _kMaxConsecutiveTurnTimeouts) {
        termination = const SessionTermination(
          SessionOutcome.budgetExhausted,
          terminationDetail: 'inference_latency',
        );
        return termination;
      }
      try {
        await runTurn();
        if (_doneRequested) {
          termination = SessionTermination(
            SessionOutcome.done,
            finalSummary: _doneReason,
          );
          return termination;
        }
      } on TurnFailure {
        // counted by runTurn — loop continues.
      } on VmServiceConnectionLost {
        termination = const SessionTermination(
          SessionOutcome.harnessError,
          harnessError: HarnessError.connectionLost,
        );
        return termination;
      } on ObservationEnvelopeError catch (e) {
        termination = SessionTermination(
          SessionOutcome.harnessError,
          harnessError: HarnessError.observationEnvelopeRejected,
          terminationDetail: 'envelope_keys=${e.keySummary}',
        );
        return termination;
      }
    }
  } catch (error) {
    // Nothing leaves runSession unclassified: record the escapee so the
    // finally clause can NAME it in the footer, then propagate it to the
    // caller exactly as before.
    escaped = error;
    rethrow;
  } finally {
    // Close writer with the appropriate footer (close() is idempotent, so
    // duplicate calls are safe if termination is already set).
    final SessionTermination t =
        termination ??
        SessionTermination(
          SessionOutcome.harnessError,
          harnessError: HarnessError.unclassified,
          terminationDetail: describeThrowable(escaped),
        );
    await _writer.close(
      SessionFooter(
        outcome: t.outcome,
        totalTurns: _turnIndex,
        totalDurationMs: _sessionStart == null
            ? 0
            : _clock().difference(_sessionStart!).inMilliseconds,
        harnessError: t.harnessError?.wireName,
        terminationDetail: t.terminationDetail,
      ),
    );
  }
}