Agents (Parallel Entities)
“I am a specific object with my own brain.”
An Agent is a stateful, persistent entity. Unlike a Worker, an Agent is not shared. When you create a new Agent, Mince spawns a dedicated Actor specifically for that object. This creates a 1-to-1 relationship: one agent instance on the main thread corresponds to exactly one actor running in a separate VM.
This makes Agents perfect for objects that need to manage their own internal state and run their own logic independently. With the ability to assign instances directly to an agent, they can become truly autonomous, controlling models and other objects without needing constant communication with the main thread.
Execution Model: Not Automatically Parallel
While the agent lives in a separate VM, when you call a method on an agent proxy, the execution happens serially within the actor’s VM. The main thread script yields until the actor function completes and returns a value.
To achieve true parallelism, the code inside your agent’s methods must manage its synchronization, such as RunService.Heartbeat:ConnectParallel() or task.desynchronize().
Interacting with Instances
While you cannot pass instances as arguments, you can give an agent control over an instance using a special method.
Agent:AssignInstance(Identifier, Instance)
This method bridges the gap between the main thread and the actor’s VM. It takes a string Identifier and an Instance reference. Under the hood, Mince creates an ObjectValue that the actor can safely access. You can remove the reference by calling the method again with nil as the Instance.
Inside the agent’s code, the instance can then be accessed from a special self.Instances table: self.Instances[Identifier].
The AssignInstance method is the only supported way to give an agent access to a DataModel instance. Do not attempt to pass instances in .New() or other methods.
Using Shared Tables
Like Workers, Agents can also have a Shared table for thread-safe communication. If you define a Shared table in your agent’s module, Mince will create it once when Mince.Agent(Module) is called.
This single Shared table is then shared across all agents created from that definition. This is a powerful feature for allowing agents of the same “class” to communicate with each other, or for the main thread to observe the collective state of an entire group of agents.
The Shared table is accessible on the agent proxy object (e.g., MyAgent.Shared).
The Agent Module
This example shows how a Shared table can be used to track a collective value across multiple agents.
local RunService = game:GetService("RunService")
local FishAgent = {
-- This table is created ONCE when Mince.Agent(script.FishAgent) is called.
-- It is shared by ALL agents created from this definition.
Shared = {
TotalDistanceTraveledByAllFish = 0,
FishCount = 0,
}
}
-- Runs ONCE inside the new actor after .New() is called.
function FishAgent:OnNew(SwimSpeed)
self.Speed = SwimSpeed
self.Target = Vector3.new() -- Initialize Target
SharedTable.increment(self.Shared, "FishCount", 1) -- Track how many fish exist
self.Connection = RunService.Heartbeat:ConnectParallel(function(DeltaTime)
self:OnStep(DeltaTime)
end)
end
function FishAgent:SetTarget(NewTarget)
self.Target = NewTarget
end
function FishAgent:OnStep(DeltaTime)
local Model = self.Instances and self.Instances.Model
if not Model or not Model.PrimaryPart then return end
local CurrentPosition = Model.PrimaryPart.Position
local Direction = (self.Target - CurrentPosition)
local Distance = self.Speed * DeltaTime
if Direction.Magnitude > 1 then
Model:SetPrimaryPartCFrame(CFrame.new(CurrentPosition + Direction.Unit * Distance))
-- Add to the collective distance traveled
SharedTable.increment(self.Shared, "TotalDistanceTraveledByAllFish", Distance)
end
end
function FishAgent:OnDestroy()
if self.Connection then
self.Connection:Disconnect()
end
SharedTable.increment(self.Shared, "FishCount", -1) -- Decrement when destroyed
print("Agent cleaned up")
end
return FishAgentUsage (Main Thread)
Because the Shared table is common to the agent definition, you can access it to see the state of the whole group.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Mince = require(ReplicatedStorage.Modules.Mince)
-- Create the Factory. This is when the Shared table is created.
local FishAgentFactory = Mince.Agent(script.FishAgent)
-- Create a few agents
local Fish1 = FishAgentFactory.New(5)
Fish1:AssignInstance("Model", workspace.FishModel1)
Fish1:SetTarget(Vector3.new(100, 0, 100))
local Fish2 = FishAgentFactory.New(7)
Fish2:AssignInstance("Model", workspace.FishModel2)
Fish2:SetTarget(Vector3.new(-100, 0, 50))
-- Read the collective state from the shared table
task.delay(5, function()
local TotalDist = FishAgentFactory.Shared.TotalDistanceTraveledByAllFish
local Count = FishAgentFactory.Shared.FishCount
print(`There are {Count} fish. Together, they have traveled {string.format("%.2f", TotalDist)} studs.`)
end)Agent Lifecycle
The lifecycle of an Agent is explicit and structured, involving a handshake between the main thread and the newly created actor.
-
Definition:
Mince.Agent(ModuleScript)- You first define an “Agent Class” by passing a ModuleScript to the
Mince.Agentconstructor.
- You first define an “Agent Class” by passing a ModuleScript to the
-
Instantiation:
AgentClass.New(...)- Calling
.New()on your definition kicks off the creation process. A newActorinstance is created.
- Calling
-
Initialization (Handshake)
- The main thread sends a
Constructmessage to the actor with any serializable arguments from.New(). - Inside the actor, a runner script calls the
OnNewfunction within your agent’s ModuleScript.
- The main thread sends a
-
Execution
- Your
Agentvariable is a proxy object. When you call a method, the proxy sends a message to the actor and yields. - The actor performs the logic serially within its VM. To run code in parallel, you must use an explicit API like
:ConnectParallel.
- Your
-
Destruction:
Agent:Destroy()- This cleans everything up, terminating the
Actorinstance.
- This cleans everything up, terminating the