run method

Executes the agent loop for goal and returns an AgentRunResult.

The loop runs synchronously in a while cycle, yielding to the event loop between iterations via AgentLoopConfig.iterationDelay.

Call stop from another isolate/coroutine to request a graceful abort.

Implementation

Future<AgentRunResult> run(AgentGoal goal) async {
  _running = true;
  _stopRequested = false;
  final startTime = DateTime.now();

  _logger.info('━━━ Agent loop START ━━━  goal: "${_trunc(goal.description)}"');

  // ── Initial plan ─────────────────────────
  var context = AgentContext(
    goal: goal,
    iterationNumber: 0,
  );

  var dag = await _planner.plan(goal, context);
  _emit(AgentLoopEventType.planned, goal.id, 0, data: _planner.summarise(dag));

  int iteration = 0;
  int consecutiveErrors = 0;

  // ── Main loop ────────────────────────────
  while (iteration < goal.maxIterations && !_stopRequested) {
    iteration++;
    _emit(AgentLoopEventType.iterationStarted, goal.id, iteration);
    _logger.info('── Iteration $iteration / ${goal.maxIterations} ──');

    // Step 1 — Observe
    context = await _stepObserve(context, goal.id, iteration);

    // Step 2 — Retrieve memory
    context = await _stepRetrieveMemory(context, goal.id, iteration);

    // Step 3 — Sync active tasks from DAG into context
    context = context.copyWith(activeTasks: dag.all);

    // Step 4 — Decide
    final decision = await _llm.reason(context);
    _emit(AgentLoopEventType.decided, goal.id, iteration, data: decision);
    _logger.info('Decision: ${decision.type.name}'
        '${decision.toolName != null ? " → ${decision.toolName}" : ""}');

    // Step 5 — Execute decision
    switch (decision.type) {
      // ── Tool call ──────────────────────
      case AgentDecisionType.useTool:
        consecutiveErrors = 0;
        final toolResult = await _stepExecuteTool(
          decision,
          context,
          goal,
          dag,
          iteration,
        );
        // Inject observation into context
        final obs = AgentObservation(
          id: _uuid.v4(),
          content: toolResult.success
              ? toolResult.outputText
              : 'Tool error: ${toolResult.error}',
          source: decision.toolName ?? 'unknown_tool',
          confidence: toolResult.success ? 0.9 : 0.3,
        );
        context = _addObservation(context, obs);

      // ── Pure thought ───────────────────
      case AgentDecisionType.think:
        consecutiveErrors = 0;
        _stepRecordThought(decision, goal, iteration);
        context = _addObservation(
          context,
          AgentObservation(
            id: _uuid.v4(),
            content: decision.thought ?? '(thinking)',
            source: 'llm_reasoning',
            confidence: decision.confidence,
          ),
        );

      // ── Goal complete ──────────────────
      case AgentDecisionType.complete:
        goal.isComplete = true;
        goal.completionReason = decision.thought;
        _memory.remember(
          content: 'Goal completed: ${goal.description}. '
              'Reason: ${decision.thought ?? "achieved"}',
          type: AgentMemoryType.goalCompletion,
          goalId: goal.id,
          importance: 1.0,
        );
        _emit(AgentLoopEventType.completed, goal.id, iteration,
            data: decision.thought);
        _logger.info('Goal COMPLETED after $iteration iteration(s)');

        await _env.dispose();
        _running = false;
        return _buildResult(
          goal: goal,
          dag: dag,
          iterations: iteration,
          success: true,
          startTime: startTime,
          summary: decision.thought ?? 'Goal achieved.',
        );

      // ── Re-plan ────────────────────────
      case AgentDecisionType.replan:
        consecutiveErrors = 0;
        final reason = decision.replanReason ?? 'Plan revision requested';
        _logger.info('Re-planning: "$reason"');
        dag = await _planner.replan(dag, context, reason: reason);
        context = context.copyWith(activeTasks: dag.all);
        _emit(AgentLoopEventType.replanned, goal.id, iteration,
            data: reason);
        _memory.remember(
          content: 'Re-planned because: $reason',
          type: AgentMemoryType.reasoning,
          goalId: goal.id,
          importance: 0.6,
        );

      // ── Error ──────────────────────────
      case AgentDecisionType.error:
        consecutiveErrors++;
        final msg = decision.thought ?? 'Unknown error';
        _logger.error('Agent error (consecutive: $consecutiveErrors): $msg');
        _memory.remember(
          content: 'Error: $msg',
          type: AgentMemoryType.failure,
          goalId: goal.id,
          importance: 0.8,
        );

        if (consecutiveErrors >= _config.maxConsecutiveErrors) {
          _logger.error(
              'Max consecutive errors reached — aborting loop');
          _emit(AgentLoopEventType.failed, goal.id, iteration,
              data: 'Max errors: $msg');
          await _env.dispose();
          _running = false;
          final abortReason =
              'Aborted after $consecutiveErrors consecutive errors.';
          return _buildResult(
            goal: goal,
            dag: dag,
            iterations: iteration,
            success: false,
            startTime: startTime,
            summary: abortReason,
            error: abortReason,
          );
        }
    }

    // Step 6 — Check if DAG is complete (all tasks succeeded/skipped)
    if (dag.isComplete && dag.size > 0) {
      _logger.info(
          'All DAG tasks complete — checking goal criteria');
      goal.isComplete = true;
      goal.completionReason = 'All planned tasks succeeded.';
      _memory.remember(
        content: 'All tasks in plan succeeded for goal: ${goal.description}',
        type: AgentMemoryType.goalCompletion,
        goalId: goal.id,
        importance: 0.9,
      );
      _emit(AgentLoopEventType.completed, goal.id, iteration);
      await _env.dispose();
      _running = false;
      return _buildResult(
        goal: goal,
        dag: dag,
        iterations: iteration,
        success: true,
        startTime: startTime,
        summary: 'All ${dag.size} task(s) completed successfully.',
      );
    }

    // Step 7 — Periodic self-reflection
    if (_config.enableReflection &&
        _reflection != null &&
        iteration % _config.reflectionIntervalIterations == 0) {
      await _stepReflect(context, goal, dag, iteration);
    }

    // Step 8 — Optional throttle
    if (_config.iterationDelay > Duration.zero) {
      await Future<void>.delayed(_config.iterationDelay);
    }
  } // end while

  // ── Fell through max iterations ──────────
  final reason = _stopRequested
      ? 'Stop requested by caller.'
      : 'Maximum iterations (${goal.maxIterations}) reached.';

  _logger.warning('Loop ended without goal completion: $reason');
  _emit(AgentLoopEventType.failed, goal.id, iteration, data: reason);

  await _env.dispose();
  _running = false;

  return _buildResult(
    goal: goal,
    dag: dag,
    iterations: iteration,
    success: false,
    startTime: startTime,
    summary: reason,
    error: reason,
  );
}