Documentation
¶
Overview ¶
Package team provides decision-driven multi-agent work. A coordinator consumes Turn and returns Decision; configured workers execute ordinary inputs and outputs. Both are exact Programs of any Strategy. Model-backed coordinators compose an interaction Strategy with a workflow adapter that renders Turn and decodes Decision. This package owns no model conversion, dispatcher, scheduler, mailbox, persistence, or product session. Construction binds the coordinator and worker Programs with their budgets and capabilities, and the Runtime starts children from those bindings. The root Descriptor owns the carried-state and final-output schemas.
ModeContinue runs another coordinator turn while workers remain active. ModeWait observes at least one drained task before running the next turn. Every wait also observes workers that finish while the current coordinator is running; those facts appear in its successor's Turn without preempting a decision. Start failures, task failures, and control rejections remain explicit facts. A failed coordinator or exhausted turn bound fails the collaboration. Turn exhaustion uses execution / collaboration.limit.turns; counter overflow uses execution / engine.counter.exhausted. Invalid coordinator decisions and protocol frames use contract / collaboration.decision.invalid and collaboration.protocol.invalid. These failures discard the candidate. A coordinator subtree with unresolved Effects cannot authorize a Decision. Worker outcomes retain their complete subtree evidence for coordinator policy. Coordinator admission and execution failures preserve the original Failure kind, code, and diagnostic; a failed turn ends the collaboration without being recorded, because the Runtime owns that Failure. The current Turn owns its number, input state, and the task outcomes that arrived after it opened; the coordinator's Turn input is assembled from these, the current tasks and controls, and the configured workers, never retained as a copy. A resolved coordinator outcome owns the Decision, including the next working state and mode; recovery derives them directly without retaining writable copies. Initial state is retained only until the first Turn takes ownership of it.
Controls compile to run.NewChildSignalEffect and run.NewChildCancelEffect. Signals obey the recipient Strategy's protocol, including interaction steering at its safe boundary. They do not preempt a model request or release an unrelated wait. For input that must wake a waiting collaboration, configure a coordination InputGate as a worker. Its addressed answer completes the gate, wakes the coordinator, and retains the original Signal identity. Re-arm a new gate when another answer is required; never redirect an ambiguous delivery to it.
Task keys identify finite executions. Follow-up work uses a new key and an explicit Input carrying prior results or application-owned context. Group discussion, round-robin selection, and peer-message routing are coordinator policies over the same task and control contracts. They do not resurrect terminal Processes or introduce a second long-lived session owner.
Completing the collaboration cancels remaining descendants. A cancellation receipt confirms intent; only a drained outcome or Handle.Join establishes resource release. Bounds complement the Runtime's monotonic tree budgets; child allocations are never refunded. Inspect execution through the Runtime's canonical tree inspection and snapshot APIs. Restore validates the strict current state against its exact frozen configuration and child contracts.
Definitions reject unaddressed Host Signals through Descriptor.SignalSchema. Runtime-owned settlements are consumed only at their matching protocol phase; Child-wait openings may share a window with the following completion. InputGate replies, when used as children, must address the child's current WaitID.
Index ¶
- Variables
- func Compile[I, O any](ctx context.Context, cfg Config) (agentrun.Program[I, O], error)
- type Config
- type Control
- type ControlReceipt
- type Decision
- type Definition
- func (d *Definition) ChildDeployments() []agentrun.Program[agentrun.Payload, agentrun.Payload]
- func (d *Definition) Descriptor() agentrun.Descriptor
- func (d *Definition) Restore(ctx context.Context, state agentrun.ExecutionState) (agentrun.Execution, error)
- func (d *Definition) Start(input agentrun.Payload) (agentrun.Execution, error)
- type DefinitionConfig
- type Mode
- type Task
- type TaskRequest
- type Turn
- type WorkerConfig
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidDecision = agentrun.NewClassifiedError( agentrun.FailureKindContract, "collaboration.decision.invalid", "collaboration: invalid decision", ) ErrInvalidExecutionState = agentrun.NewClassifiedError( agentrun.FailureKindContract, "collaboration.state.invalid", "collaboration: invalid execution state", ) ErrInvalidProtocol = agentrun.NewClassifiedError( agentrun.FailureKindContract, "collaboration.protocol.invalid", "collaboration: invalid execution protocol", ) ErrTurnLimit = agentrun.NewClassifiedError( agentrun.FailureKindExecution, "collaboration.limit.turns", "collaboration: turn limit reached", ) )
Execution sentinels own the Failure persisted when they reach Step. Wrapping one is the only way a Step error acquires its classification.
var ErrInvalidConfig = errors.New("collaboration: invalid configuration")
ErrInvalidConfig rejects construction before execution and therefore carries no Failure classification.
Functions ¶
Types ¶
type Config ¶
type Config struct {
Name string
Description string
Coordinator WorkerConfig
Workers []WorkerConfig
// MaxTurns is unlimited by default; a finite zero cannot admit the first turn.
MaxTurns agentrun.Quota
// MaxTasks is unlimited by default; finite zero forbids worker startups.
MaxTasks agentrun.Quota
MaxConcurrentTasks uint32
MaxControlsPerTurn uint32
BuildIdentity agentrun.Digest
}
Config grants the coordinator and workers their exact programs and resource authority. Input and output schemas come from Compile's type parameters.
type Control ¶
type Control struct {
Task agentrun.ChildKey `json:"task"`
Signal *agentrun.SignalRequest `json:"signal,omitzero"`
CancelReason *string `json:"cancel_reason,omitzero"`
}
Control targets an already admitted task. Exactly one of Signal and CancelReason is present. Signal uses the recipient's own input contract and caller-stable identity; cancellation is intent, not proof of resource release.
type ControlReceipt ¶
type ControlReceipt struct {
Control Control `json:"control"`
Result *agentrun.ChildControlResult `json:"result,omitzero"`
}
type Decision ¶
type Decision struct {
Mode Mode `json:"mode"`
State agentrun.Payload `json:"state"`
Tasks []TaskRequest `json:"tasks,omitempty"`
Controls []Control `json:"controls,omitempty"`
Output agentrun.Payload `json:"output,omitzero"`
}
Decision is the coordinator's output contract. The entire batch is validated before any action is declared. A present Output completes the collaboration and cancels unfinished descendants; it admits no Mode or actions. Otherwise Mode chooses how the next turn starts. An explicit JSON null is a present Output. Input and Output retain the configured domain schemas.
type Definition ¶
type Definition struct {
// contains filtered or unexported fields
}
Definition coordinates background tasks through ordinary child Effects. It owns decision policy, not a scheduler, mailbox, model client, or session.
Example ¶
The deterministic local model exercises the same interaction Dispatcher a provider model would use. The role owns the application's prompt and typed-output adaptation; collaboration only consumes Turn and Decision.
package main
import (
startrand "crypto/rand"
"context"
jsonv2 "encoding/json/v2"
"errors"
"fmt"
"github.com/Tangerg/scope/agent"
"github.com/Tangerg/scope/agent/coordination"
"github.com/Tangerg/scope/agent/run"
"github.com/Tangerg/scope/agent/team"
"github.com/Tangerg/scope/agent/workflow"
"github.com/Tangerg/scope/core/chat"
"github.com/Tangerg/scope/core/chatclient"
"github.com/Tangerg/scope/core/jsonschema"
)
// The deterministic local model exercises the same interaction Dispatcher a
// provider model would use. The role owns the application's prompt and
// typed-output adaptation; collaboration only consumes Turn and Decision.
func main() {
ctx := context.Background()
textSchema := exampleValue(jsonschema.For[string]())
client := exampleValue(chatclient.New(decisionModel{}, chatclient.Config{}))
model := exampleValue(agent.Compile(ctx, agent.Config{Name: "example.decision_model", Description: "Choose the next team decision.", MaxModelCalls: run.NewQuota(1), Model: client, BuildIdentity: run.ComputeDigest([]byte("example decision build")), ModelIdentity: run.ComputeDigest([]byte("example decision settings"))}))
budget := run.Budget{Steps: run.NewQuota(32), Effects: run.NewQuota(16), Signals: run.NewQuota(32)}
coordinator := exampleCoordinator(model, budget)
gate := exampleBinding(exampleValue(coordination.NewInputGate(coordination.InputGateConfig{
Name: "example.input", Description: "Wait for an external instruction.", RequestSchema: textSchema, AnswerSchema: textSchema,
})), nil)
work := exampleValue(workflow.Transform("review", func(_ context.Context, task string) (string, error) {
return "reviewed: " + task, nil
}))
worker := exampleBinding(exampleValue(workflow.NewDefinition(workflow.DefinitionConfig{
Name: "example.reviewer", Description: "Review a task.", Stages: []workflow.Stage{work},
})), nil)
root := exampleValue(team.Compile[string, string](ctx, team.Config{BuildIdentity: run.ComputeDigest([]byte("example team")),
Name: "example.collaboration", Description: "Run model decisions alongside background work.",
Coordinator: team.WorkerConfig{Deployment: coordinator, Budget: run.Budget{Steps: run.NewQuota(64), Effects: run.NewQuota(32), Signals: run.NewQuota(64)}},
Workers: []team.WorkerConfig{{Deployment: gate, Budget: budget}, {Deployment: worker, Budget: budget}},
MaxTurns: run.NewQuota(4), MaxTasks: run.NewQuota(2), MaxConcurrentTasks: 2, MaxControlsPerTurn: 1,
}))
engine := exampleValue(run.New(context.Background(), run.Config{Store: run.NewMemoryStore()}))
defer func() {
if err := engine.Close(ctx); err != nil {
panic(err)
}
}()
process := exampleValue(engine.Start(ctx, root, run.StartRequest[string]{ID: run.StartID{Scope: "test", Key: startrand.Text()}, Input: "inspect deployment"}))
result := exampleValue(process.Await(ctx))
if err := process.Join(ctx); err != nil {
panic(err)
}
if result.Termination().Status() != run.StatusCompleted {
panic(result.Termination().Reason())
}
output, _ := result.Termination().Output()
fmt.Println(exampleValue(output.Decode[string]()))
}
func exampleCoordinator(model run.Program[agent.Input, agent.Output], budget run.Budget) run.Program[run.Payload, run.Payload] {
return exampleValue(agent.Adapt(context.Background(), agent.AdapterConfig[team.Turn, team.Decision]{
Name: "example.coordinator", Description: "Render a turn and decode a completed model decision.",
Interaction: model, Budget: budget,
BuildInput: func(_ context.Context, turn team.Turn) (agent.Input, error) {
payload, err := jsonv2.Marshal(turn)
if err != nil {
return agent.Input{}, err
}
return agent.Input{Messages: []chat.Message{chat.NewUserMessage(chat.NewTextPart(string(payload)))}}, nil
},
DecodeOutput: func(_ context.Context, output agent.Output) (team.Decision, error) {
return chatclient.JSON[team.Decision]().Decode(output.ModelResponse)
},
ImplementationDigest: run.ComputeDigest([]byte("collaboration-coordinator-role")),
ConfigurationDigest: run.ComputeDigest(exampleValue(jsonv2.Marshal(budget))),
})).Erase()
}
type decisionModel struct{}
func (d decisionModel) Call(_ context.Context, request *chat.Request) (*chat.Response, error) {
if len(request.Messages) != 1 {
return nil, errors.New("expected one rendered turn")
}
input, err := run.ParsePayload([]byte(request.Messages[0].Text()))
if err != nil {
return nil, err
}
turn, err := input.Decode[team.Turn]()
if err != nil {
return nil, err
}
decision, err := d.decide(turn)
if err != nil {
return nil, err
}
payload, err := jsonv2.Marshal(decision)
if err != nil {
return nil, err
}
message := chat.NewAssistantMessage(chat.NewTextPart(string(payload)))
return chat.NewResponse(&chat.Output{Message: &message, FinishReason: chat.FinishReasonStop}, nil)
}
func (d decisionModel) decide(turn team.Turn) (team.Decision, error) {
decision := team.Decision{State: turn.State}
switch turn.Number {
case 1:
decision.Mode = team.ModeContinue
decision.Tasks = []team.TaskRequest{{Key: exampleValue(run.ParseChildKey("input")), Worker: "example.input", Input: turn.State}}
case 2:
if len(turn.Tasks) != 1 || turn.Tasks[0].Start == nil || turn.Tasks[0].Outcome != nil {
return decision, errors.New("background input task was not outstanding")
}
decision.Mode = team.ModeWait
reason := "No replacement instruction is needed."
decision.Controls = []team.Control{{Task: turn.Tasks[0].Request.Key, CancelReason: &reason}}
case 3:
if turn.Tasks[0].Outcome == nil || turn.Tasks[0].Outcome.Result().Termination().Status() != run.StatusCanceled {
return decision, errors.New("input cancellation did not drain")
}
decision.Mode = team.ModeWait
decision.Tasks = []team.TaskRequest{{Key: exampleValue(run.ParseChildKey("review")), Worker: "example.reviewer", Input: turn.State}}
case 4:
if len(turn.Tasks) != 2 || turn.Tasks[1].Outcome == nil {
return decision, errors.New("review outcome is missing")
}
output, present := turn.Tasks[1].Outcome.Result().Termination().Output()
if !present {
return decision, errors.New("review produced no output")
}
review, err := output.Decode[string]()
if err != nil {
return decision, err
}
final, err := run.EncodePayload("coordinator continued while input was pending; " + review)
if err != nil {
return decision, err
}
decision.Mode, decision.Output = team.ModeUndecided, final
default:
return decision, errors.New("unexpected coordinator turn")
}
return decision, nil
}
func exampleValue[T any](value T, err error) T {
if err != nil {
panic(err)
}
return value
}
func exampleBinding(definition run.Definition, dispatcher run.Dispatcher) run.Program[run.Payload, run.Payload] {
// These labels identify the fixed code and configuration in this checked
// example. Production bindings identify the built artifact and full config.
return exampleValue(run.Bind[run.Payload, run.Payload](run.BindingConfig{
Definition: definition, Dispatcher: dispatcher,
ImplementationDigest: run.ComputeDigest([]byte("collaboration-example-artifact")),
ConfigurationDigest: run.ComputeDigest([]byte(definition.Descriptor().Name())),
}))
}
Output: coordinator continued while input was pending; reviewed: inspect deployment
func NewDefinition ¶
func NewDefinition(config DefinitionConfig) (*Definition, error)
func (*Definition) ChildDeployments ¶
ChildDeployments reports the coordinator and every worker binding.
func (*Definition) Descriptor ¶
func (d *Definition) Descriptor() agentrun.Descriptor
func (*Definition) Restore ¶
func (d *Definition) Restore(ctx context.Context, state agentrun.ExecutionState) (agentrun.Execution, error)
type DefinitionConfig ¶
type DefinitionConfig struct {
Name string
Description string
Coordinator WorkerConfig
Workers []WorkerConfig
StateSchema jsonschema.Schema
OutputSchema jsonschema.Schema
// MaxTurns is unlimited by default; a finite zero cannot admit the first turn.
MaxTurns agentrun.Quota
// MaxTasks is unlimited by default; a finite zero forbids worker starts.
MaxTasks agentrun.Quota
MaxConcurrentTasks uint32
// MaxControlsPerTurn bounds each Decision's controls independently of
// cumulative work.
MaxControlsPerTurn uint32
}
DefinitionConfig binds the coordinator, permitted workers, schemas, and finite collaboration bounds. Coordinator must accept Turn and return Decision with exactly their core/jsonschema.For contracts. It may itself be any Strategy. MaxTasks counts all attempts, including rejected starts. MaxConcurrentTasks counts admitted tasks until their drained outcomes have been observed. The Program configuration digest must cover every field; the coordinator and worker bindings enter the Program identity through ChildDeployments.
type Mode ¶
type Mode string
const ( ModeUndecided Mode = "" // ModeContinue starts the next turn after action receipts, while tasks run. ModeContinue Mode = "continue" // ModeWait starts the next turn after at least one outstanding task drains. // If no tasks remain outstanding, failed-start receipts trigger the next turn. // Older outstanding tasks still delay the turn when every new start fails; // use ModeContinue to process action receipts without waiting for those tasks. ModeWait Mode = "wait" )
type Task ¶
type Task struct {
Request TaskRequest `json:"request"`
Start *agentrun.ChildStartResult `json:"start,omitzero"`
Outcome *agentrun.ChildOutcome `json:"outcome,omitzero"`
}
Task retains one request and its canonical kernel lifecycle facts. A task with neither Start nor Outcome is pending admission; Start alone is a failed admission or an outstanding child; Outcome replaces Start once the child drains, and its Result names the child.
type TaskRequest ¶
type TaskRequest struct {
Key agentrun.ChildKey `json:"key"`
Worker string `json:"worker"`
Input agentrun.Payload `json:"input"`
}
TaskRequest starts a new bounded execution of a configured worker. To follow up on a completed task, choose a new Key and carry the previous result in Input. Terminal Processes are immutable; continuation has a new lifecycle.
type Turn ¶
type Turn struct {
Number uint64 `json:"number"`
State agentrun.Payload `json:"state"`
Workers []agentrun.Descriptor `json:"workers"`
Tasks []Task `json:"tasks"`
Controls []ControlReceipt `json:"controls"`
}
Turn is the coordinator's complete portable input. Tasks are cumulative and request ordered. Controls contains the previous decision's action receipts. State is explicit working memory; a Decision replaces it in full. Workers exposes the exact configured input/output contracts without execution grants. Facts arriving while this turn runs are visible in the following turn.
type WorkerConfig ¶
type WorkerConfig struct {
Deployment agentrun.Program[agentrun.Payload, agentrun.Payload]
Budget agentrun.Budget
Capabilities agentrun.CapabilitySet
}
WorkerConfig freezes a child binding and its permanently allocated grants. Workers are selected by Program.Descriptor().Name(), never by routing or a model-supplied ProgramRef, budget, or capability grant. The Definition owns the bound Programs as its ChildDeployments.