Source review and production activation have separate evidence; the status here records what the corresponding live canary actually proved.
Passed the bounded bead-to-agent canary; the service returned to shadow mode after the run. Shown at the current implementation head rather than the canary revision.
How to read this file
Follow the execution boundary
Click any line section to open its explanation. The complete file stays visible, and the selected explanation opens directly beneath the code it describes.
package temporalbeads
import (
"fmt"
"sync"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
)
// WorkerSet registers deterministic orchestration and nondeterministic agent
// execution on separate Task Queues.
type WorkerSet struct {
mu sync.Mutex
orchestration worker.Worker
agent worker.Worker
started bool
stopped bool
}
// NewWorkerSet constructs both workers without starting external processes.Lines 1–23
Own two Worker pollers
- What this section does
- Keeps the orchestration Worker and the agent Activity Worker under one small lifecycle object.
- Why it matters for Temporal
- Separate Task Queues allow the two workloads to be deployed and scaled independently, although this WorkerSet currently co-locates both pollers.
- Two Task Queues
- Lifecycle state
func NewWorkerSet(
temporalClient client.Client,
beads BeadStore,
agent AgentExecutor,
) (*WorkerSet, error) {
if temporalClient == nil {
return nil, fmt.Errorf("temporal client is required")
}
if beads == nil {
return nil, fmt.Errorf("beads store is required")
}
if agent == nil {
return nil, fmt.Errorf("agent executor is required")
}
activities := &ActivityWorker{Beads: beads, Agent: agent}
return newWorkerSet(temporalClient, activities.ExecuteBead)
}
// NewShadowWorkerSet polls the production Task Queues while rejecting every
// agent Activity before it can touch Beads or dispatch an agent.
func NewShadowWorkerSet(temporalClient client.Client) (*WorkerSet, error) {
if temporalClient == nil {
return nil, fmt.Errorf("temporal client is required")
}
activities := &ShadowActivityWorker{}
return newWorkerSet(temporalClient, activities.ExecuteBead)
}
Lines 24–51
Choose canary or shadow dependencies
- What this section does
- Builds a real Worker set only with a work store and agent executor, while shadow mode binds an Activity that rejects execution before mutation.
- Why it matters for Temporal
- The same Workflow registration can be observed in deployment without accidentally dispatching an agent.
- Dependency checks
- Shadow mode
- Fail closed
func newWorkerSet(
temporalClient client.Client,
executeBead interface{},
) (*WorkerSet, error) {
orchestrationWorker := worker.New(
temporalClient,
OrchestrationTaskQueue,
worker.Options{},
)
orchestrationWorker.RegisterWorkflowWithOptions(
BeadOrchestrationWorkflow,
workflow.RegisterOptions{Name: BeadOrchestrationWorkflowName},
)
agentWorker := worker.New(temporalClient, AgentTaskQueue, worker.Options{})
agentWorker.RegisterActivityWithOptions(
executeBead,
activity.RegisterOptions{Name: ExecuteBeadActivityName},
)
return &WorkerSet{
orchestration: orchestrationWorker,
agent: agentWorker,
}, nil
}
// Start begins both pollers and rolls back if the second start fails.Lines 52–76
Register by explicit names
- What this section does
- Registers the Workflow and Activity on their dedicated Task Queues using stable public names.
- Why it matters for Temporal
- Explicit names and Task Queues form a deployment contract that survives Go symbol refactors.
- Stable names
- Workflow registration
- Activity registration
func (s *WorkerSet) Start() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.started {
return nil
}
if s.stopped {
return fmt.Errorf("worker set has been stopped and cannot be restarted")
}
if err := s.orchestration.Start(); err != nil {
return fmt.Errorf("start orchestration worker: %w", err)
}
if err := s.agent.Start(); err != nil {
s.orchestration.Stop()
s.stopped = true
return fmt.Errorf("start agent worker: %w", err)
}
s.started = true
return nil
}
// Stop stops both Task Queue pollers.
func (s *WorkerSet) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
if s.stopped {
return
}
s.stopped = true
if !s.started {
return
}
s.agent.Stop()
s.orchestration.Stop()
s.started = false
}Lines 77–112
Start and stop as one unit
- What this section does
- Starts both pollers, rolls back a partial start, and makes repeated lifecycle calls safe.
- Why it matters for Temporal
- A managed service should not leave one Task Queue polling after the other Worker fails to start.
- Rollback
- Idempotent lifecycle