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,
);
}