Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions AgctorSDK.Agents/Agents/Actor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace AgctorSDK.Core.Agents
{
/// <summary>
/// Actor base whose decisions go through <see cref="BaseActor.Context"/>.
/// Subclasses call <c>Decide</c> from a message handler; the runtime selects the engine.
/// </summary>
public abstract class Actor : BaseActor
{
protected Actor(string id)
: base(id)
{
}

protected Actor(string id, string actorType)
: base(id, actorType)
{
}
}
}
23 changes: 22 additions & 1 deletion AgctorSDK.Agents/Agents/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AgctorSDK.Core.Decisions;
using AgctorSDK.Core.Interfaces;
using AgctorSDK.Core.Messages;
using System.Text.RegularExpressions;
Expand All @@ -16,9 +17,10 @@
/// Basic implementation of an intelligent agent that can process prompts and spawn child agents.
/// Provides recursive task decomposition capabilities and manages a hierarchy of child agents.
/// </summary>
public class Agent : IAgent
public class Agent : IAgent, IHasActorContext
{
private readonly List<string> _childAgentIds = new();
private readonly ActorContextSlot _decisions = new ActorContextSlot();
private readonly object _lockObject = new();
private IAgentFactory? _agentFactory;
private string? _currentPrompt;
Expand Down Expand Up @@ -77,6 +79,25 @@
/// </summary>
public AgentStatus Status => _status;

/// <summary>
/// Decision gateway bound by the runtime. Calls go to the decision actor.
/// </summary>
public IActorContext? Context => _decisions.Context;

/// <inheritdoc />
public void BindContext(IActorContext context)
{
_decisions.BindContext(context);
}

/// <summary>
/// Asks the decision fabric from inside this agent.
/// </summary>
protected Task<DecisionResult> Decide(DecisionRequest request, CancellationToken cancellationToken = default)
{
return _decisions.Decide(request, cancellationToken);
}

/// <summary>
/// Gets the current hierarchy depth of this agent.
/// </summary>
Expand Down Expand Up @@ -124,7 +145,7 @@
/// </summary>
/// <param name="id">The unique identifier for this agent</param>
/// <param name="serviceProvider">Optional service provider for dependency injection</param>
public Agent(string id, IServiceProvider? serviceProvider = null)

Check warning on line 148 in AgctorSDK.Agents/Agents/Agent.cs

View workflow job for this annotation

GitHub Actions / Build and test

Non-nullable property 'ErrorHandler' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 148 in AgctorSDK.Agents/Agents/Agent.cs

View workflow job for this annotation

GitHub Actions / Build and test

Non-nullable property 'Logger' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
Id = id ?? throw new ArgumentNullException(nameof(id));
InitializeLoggingServices(serviceProvider);
Expand All @@ -134,7 +155,7 @@
/// Parameterless constructor for reflection-based instantiation.
/// The ID will be set via reflection or during initialization.
/// </summary>
public Agent()

Check warning on line 158 in AgctorSDK.Agents/Agents/Agent.cs

View workflow job for this annotation

GitHub Actions / Build and test

Non-nullable property 'ErrorHandler' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 158 in AgctorSDK.Agents/Agents/Agent.cs

View workflow job for this annotation

GitHub Actions / Build and test

Non-nullable property 'Logger' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
// ID will be set during initialization
InitializeLoggingServices();
Expand Down Expand Up @@ -615,7 +636,7 @@
// Special handling for CodeEditorTool to forward content from previous step
if (agentType == "CodeEditorTool" && _lastSubtaskResult != null)
{
string content = null;

Check warning on line 639 in AgctorSDK.Agents/Agents/Agent.cs

View workflow job for this annotation

GitHub Actions / Build and test

Converting null literal or possible null value to non-nullable type.

// Extract content from previous result based on its type
if (_lastSubtaskResult is string stringResult)
Expand Down
31 changes: 29 additions & 2 deletions AgctorSDK.Agents/Agents/BaseActor.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using AgctorSDK.Core.Decisions;
using AgctorSDK.Core.Interfaces;
using AgctorSDK.Core.Messages;

namespace AgctorSDK.Core.Agents
{
public abstract class BaseActor : IActor
public abstract class BaseActor : IActor, IHasActorContext
{
private readonly ActorContextSlot _decisions = new ActorContextSlot();

public string Id { get; }
public string ActorType { get; }
public ActorState State { get; protected set; }

/// <summary>
/// Runtime-bound gateway. Decide goes to the decision actor, not to a provider.
/// </summary>
public IActorContext? Context => _decisions.Context;

public event EventHandler<ActorStateChangedEventArgs>? StateChanged;

protected BaseActor(string id)
: this(id, string.Empty)
{
}

protected BaseActor(string id, string actorType)
{
Id = id;
ActorType = actorType;
// GetType() is the concrete actor, so a one-arg constructor still reports the right type.
ActorType = string.IsNullOrWhiteSpace(actorType) ? GetType().Name : actorType;
State = ActorState.Initializing;
}

public void BindContext(IActorContext context)
{
_decisions.BindContext(context);
}

/// <summary>
/// Asks the decision fabric. Available only after the runtime binds <see cref="Context"/>.
/// </summary>
protected Task<DecisionResult> Decide(DecisionRequest request, CancellationToken cancellationToken = default)
{
return _decisions.Decide(request, cancellationToken);
}

public virtual Task InitializeAsync(CancellationToken cancellationToken = default)
{
ChangeState(ActorState.Active, "Initialized successfully");
Expand Down
95 changes: 88 additions & 7 deletions AgctorSDK.Agents/Runtime/InMemoryActorRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using AgctorSDK.Core.Actors;
using AgctorSDK.Core.Decisions;
using AgctorSDK.Core.Interfaces;
using AgctorSDK.Core.Messages;
using AgctorSDK.Core.Agents;
Expand All @@ -26,17 +28,25 @@ public class InMemoryActorRuntime : IActorRuntimeAdapter
private readonly CancellationTokenSource _shutdownTokenSource = new();
private readonly IAgctorLogger _logger;
private readonly ErrorHandlingMiddleware _errorHandler;

private readonly IDecisionService? _decisions;
private readonly object _decisionGate = new();
private Task? _decisionActorTask;

public InMemoryActorRuntime()
: this(Utils.Logging.LoggerFactory.CreateLogger("InMemoryActorRuntime"), null)
{
_logger = Utils.Logging.LoggerFactory.CreateLogger("InMemoryActorRuntime");
_errorHandler = new ErrorHandlingMiddleware(_logger);
}

public InMemoryActorRuntime(IAgctorLogger logger)
: this(logger, null)
{
}

public InMemoryActorRuntime(IAgctorLogger logger, IDecisionService? decisions)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_errorHandler = new ErrorHandlingMiddleware(_logger);
_decisions = decisions;
}

private bool _isInitialized;
Expand Down Expand Up @@ -75,14 +85,14 @@ public ActorInstance(IActor actor, Channel<IMessageEnvelope> messageQueue,
}
}

public Task InitializeAsync(IDictionary<string, object> configuration, CancellationToken cancellationToken = default)
public async Task InitializeAsync(IDictionary<string, object> configuration, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();

if (_isInitialized)
{
LogTrace("Runtime already initialized");
return Task.CompletedTask;
return;
}

LogTrace("Initializing InMemoryActorRuntime...");
Expand All @@ -100,8 +110,10 @@ public Task InitializeAsync(IDictionary<string, object> configuration, Cancellat
_startTime = DateTimeOffset.UtcNow;
_isInitialized = true;

// The decision actor has to exist before any other actor calls Context.Decide.
await EnsureDecisionActorAsync(cancellationToken).ConfigureAwait(false);

LogTrace($"InMemoryActorRuntime initialized successfully at {_startTime}");
return Task.CompletedTask;
}

public async Task ShutdownAsync(CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -160,6 +172,11 @@ public async Task<T> SpawnActorAsync<T>(string actorId, Func<string, T> actorFac
throw new ArgumentException("Actor ID cannot be null or empty.", nameof(actorId));
}

if (!IsDecisionActor(actorId))
{
await EnsureDecisionActorAsync(cancellationToken).ConfigureAwait(false);
}

var newActorInstance = actorFactory(actorId);

// Setup agent-specific properties before initialization
Expand All @@ -179,6 +196,12 @@ public async Task<T> SpawnActorAsync<T>(string actorId, Func<string, T> actorFac
}
}
}

// Bind before InitializeAsync so an actor can decide while it is starting.
if (!IsDecisionActor(actorId))
{
BindDecisionContext(newActorInstance);
}

await newActorInstance.InitializeAsync(cancellationToken);

Expand Down Expand Up @@ -229,6 +252,12 @@ public async Task RegisterActorAsync(IActor actor, CancellationToken cancellatio
throw new ArgumentException("Actor ID cannot be null or empty.", nameof(actor));
}

if (!IsDecisionActor(actorId))
{
await EnsureDecisionActorAsync(cancellationToken).ConfigureAwait(false);
BindDecisionContext(actor);
}

var messageQueue = Channel.CreateUnbounded<IMessageEnvelope>(new UnboundedChannelOptions
{
SingleReader = true, // Each actor has its own processing loop
Expand Down Expand Up @@ -548,6 +577,58 @@ protected virtual void Dispose(bool disposing)
_isDisposed = true;
}

private static bool IsDecisionActor(string actorId)
{
return string.Equals(actorId, DecisionFabricIds.DecisionActorId, StringComparison.Ordinal);
}

/// <summary>
/// Spawns the decision actor once. Other spawns await the same task so Context is never bound
/// before the mailbox that serves Decide exists.
/// </summary>
private Task EnsureDecisionActorAsync(CancellationToken cancellationToken)
{
if (_decisions == null)
{
return Task.CompletedTask;
}

if (_actors.ContainsKey(DecisionFabricIds.DecisionActorId))
{
return Task.CompletedTask;
}

lock (_decisionGate)
{
if (_actors.ContainsKey(DecisionFabricIds.DecisionActorId))
{
return Task.CompletedTask;
}

if (_decisionActorTask == null)
{
var decisions = _decisions;
_decisionActorTask = SpawnActorAsync(
DecisionFabricIds.DecisionActorId,
id => new DecisionActor(id, decisions),
cancellationToken: cancellationToken);
}

return _decisionActorTask;
}
}

private void BindDecisionContext(IActor actor)
{
if (_decisions == null || actor is not IHasActorContext slot)
{
return;
}

// The actor asks the decision actor by id. It never receives a provider reference.
slot.BindContext(new ActorContext(actor.Id, actor.ActorType, this));
}

private T CreateActorInstance<T>(string actorId) where T : class, IActor
{
try
Expand Down
Loading
Loading