run method

Future<String?> run(
  1. String goal, {
  2. required AgentMode mode,
  3. AgentAbort? abort,
})

Runs the agent toward goal under mode. Returns the model's final summary text (also already written via AgentHandlers.writeLine), or null if the agent was aborted/cancelled or hit the step cap without concluding.

abort lets the host stop the run (Ctrl-C or an abort answer at a prompt); the loop checks it at each safe point and bails out of a pending model call, confirming first unless AgentAbort.isConfirmed.

Implementation

Future<String?> run(
  String goal, {
  required AgentMode mode,
  AgentAbort? abort,
}) async {
  final cancel = abort ?? AgentAbort();
  final messages = <AiMessage>[
    AiMessage.system(_systemPrompt(mode)),
    AiMessage.user(goal),
  ];

  // In plan mode, tracks the approval verdict once a plan is accepted.
  PlanApproval? planApproval;

  _lastCommandMutated = false;
  _replanNeeded = false;
  _announcedExecutor = false;
  _runUsage = AiUsage.zero;
  _runRequests = 0;
  _runClock
    ..reset()
    ..start();

  _frame(); // a horizontal rule opening the interaction

  final aborted = style.blocked('ai: aborted.');

  for (var step = 0; step < config.maxSteps; step++) {
    if (await _aborted(cancel)) return _finish(aborted);

    final phase = _phaseFor(mode: mode, planApproval: planApproval);
    final model = config.modelFor(phase);
    _maybeAnnounceExecutor(phase);

    final AiResult result;
    try {
      // Race the model call against an abort so Ctrl-C during a slow request
      // is responsive; the request itself is abandoned (its result ignored).
      final chat = provider.chat(
        messages: messages,
        tools: _tools,
        model: model,
      );
      final won = await Future.any<bool>([
        chat.then((_) => true),
        cancel.whenRequested.then((_) => false),
      ]);
      if (!won) {
        if (await _aborted(cancel)) return _finish(aborted);
      }
      result = await chat; // resolved (won) or user declined the abort
    } on AiProviderException catch (e) {
      return _finish('ai: provider error: ${e.message}');
    }

    if (result.usage != null) {
      _runUsage += result.usage!;
      _runRequests++;
    }

    final text = result.text?.trim();
    messages.add(
      AiMessage.assistant(text: result.text, toolCalls: result.toolCalls),
    );

    final styledText = (text == null || text.isEmpty)
        ? null
        : (phase == AgentPhase.executing
              ? style.executing(text)
              : style.planning(text));

    if (!result.wantsTools) {
      // Final answer for this turn: a blank line then the styled summary
      // (same framing as _finish), then a preliminary stats line so the
      // running token totals are visible before the continue/end prompt.
      // Offer to keep chatting in the same context before closing.
      handlers.writeLine('');
      if (styledText != null) handlers.writeLine(styledText);
      _writeStats();
      final followUp = await _promptContinue();
      if (followUp == null) {
        _frame(); // closing rule (stats already shown above)
        return text;
      }
      // Continue in the same context: append the user turn, reset the
      // in-flight plan state so a new sub-goal re-investigates/re-plans, and
      // give it a fresh step budget.
      messages.add(AiMessage.user(followUp));
      planApproval = null;
      _lastCommandMutated = false;
      _replanNeeded = false;
      step = -1; // the for-loop ++ makes the next iteration step 0
      continue;
    }

    if (styledText != null) handlers.writeLine(styledText);

    var cancelled = false;
    for (final call in result.toolCalls) {
      final outcome = await _handleToolCall(
        call,
        mode: mode,
        planApproval: planApproval,
        onPlanApproval: (a) => planApproval = a,
      );
      messages.add(AiMessage.tool(outcome.result));
      if (outcome.cancelled) cancelled = true;
    }
    if (cancelled) return _finish(null);
    if (await _aborted(cancel)) return _finish(aborted);
  }

  return _finish(
    'ai: stopped after ${config.maxSteps} steps without finishing.',
  );
}