Documentation
¶
Overview ¶
Package collaboration provides decision-driven multi-agent work. A coordinator consumes Turn and returns Decision; configured workers execute ordinary inputs and outputs. Both are exact Deployments 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 Deployments with their budgets and capabilities, and the Engine 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; the failed turn remains restorable evidence. The current Turn owns its number and input state. 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 agent.NewChildSignalEffect and agent.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 Process.Join establishes resource release. Bounds complement the Engine's monotonic tree budgets; child allocations are never refunded. Inspect execution through the Engine'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. Engine-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 ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidDecision = agent.NewClassifiedError( agent.FailureKindContract, "collaboration.decision.invalid", "collaboration: invalid decision", ) ErrInvalidExecutionState = agent.NewClassifiedError( agent.FailureKindContract, "collaboration.state.invalid", "collaboration: invalid execution state", ) ErrInvalidProtocol = agent.NewClassifiedError( agent.FailureKindContract, "collaboration.protocol.invalid", "collaboration: invalid execution protocol", ) ErrTurnLimit = agent.NewClassifiedError( agent.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 ¶
This section is empty.
Types ¶
type Control ¶
type Control struct {
Task agent.ChildKey `json:"task"`
Signal *agent.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 *agent.ChildControlResult `json:"result,omitzero"`
}
type Decision ¶
type Decision struct {
Mode Mode `json:"mode"`
State agent.Payload `json:"state"`
Tasks []TaskRequest `json:"tasks,omitempty"`
Controls []Control `json:"controls,omitempty"`
Output agent.Payload `json:"output,omitzero"`
}
Decision is the coordinator's output contract. The entire batch is validated before any action is declared. ModeComplete requires Output and no actions; other modes require a zero Output. 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 workflow owns the application's prompt and typed-output adaptation; collaboration only consumes Turn and Decision.
package main
import (
"context"
jsonv2 "encoding/json/v2"
"errors"
"fmt"
agent "github.com/Tangerg/scope/agent"
"github.com/Tangerg/scope/agent/strategy/collaboration"
"github.com/Tangerg/scope/agent/strategy/coordination"
"github.com/Tangerg/scope/agent/strategy/interaction"
"github.com/Tangerg/scope/agent/strategy/workflow"
"github.com/Tangerg/scope/core/chat"
"github.com/Tangerg/scope/core/chatclient"
)
// The deterministic local model exercises the same interaction Dispatcher a
// provider model would use. The workflow owns the application's prompt and
// typed-output adaptation; collaboration only consumes Turn and Decision.
func main() {
ctx := context.Background()
textSchema := exampleValue(agent.SchemaFor[string]())
modelDefinition := exampleValue(interaction.NewDefinition(interaction.DefinitionConfig{
Name: "example.decision_model", Description: "Choose the next collaboration action.", MaxModelCalls: agent.NewQuota(1),
}))
client := exampleValue(chatclient.New(decisionModel{}, chatclient.Config{}))
dispatcher := exampleValue(interaction.NewDispatcher(modelDefinition, interaction.DispatcherConfig{Model: client}))
model := exampleBinding(modelDefinition, dispatcher)
budget := agent.Budget{Steps: agent.NewQuota(32), Effects: agent.NewQuota(16), Signals: agent.NewQuota(32)}
render := exampleValue(workflow.Transform("render_turn", func(_ context.Context, turn collaboration.Turn) (interaction.Input, error) {
payload, err := jsonv2.Marshal(turn)
if err != nil {
return interaction.Input{}, err
}
return interaction.Input{Messages: []chat.Message{chat.NewUserMessage(chat.NewTextPart(string(payload)))}}, nil
}))
call := exampleValue(workflow.Call(workflow.CallConfig{ID: "decide", Deployment: model, Budget: budget}))
decode := exampleValue(workflow.Transform("decode_decision", func(_ context.Context, output interaction.Output) (collaboration.Decision, error) {
if output.Source != interaction.CompletionSourceModelResponse || output.ModelResponse == nil {
return collaboration.Decision{}, errors.New("coordinator produced no model response")
}
value, err := agent.ParsePayload([]byte(output.ModelResponse.Text()))
if err != nil {
return collaboration.Decision{}, err
}
return value.Decode[collaboration.Decision]()
}))
coordinator := exampleBinding(exampleValue(workflow.NewDefinition(workflow.DefinitionConfig{
Name: "example.coordinator", Description: "Render a turn and decode a model decision.", Stages: []workflow.Stage{render, call, decode},
})), nil)
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)
definition := exampleValue(collaboration.NewDefinition(collaboration.DefinitionConfig{
Name: "example.collaboration", Description: "Run model decisions alongside background work.",
Coordinator: collaboration.WorkerConfig{Deployment: coordinator, Budget: agent.Budget{Steps: agent.NewQuota(64), Effects: agent.NewQuota(32), Signals: agent.NewQuota(64)}},
Workers: []collaboration.WorkerConfig{{Deployment: gate, Budget: budget}, {Deployment: worker, Budget: budget}},
StateSchema: textSchema, OutputSchema: textSchema, MaxTurns: agent.NewQuota(4), MaxTasks: agent.NewQuota(2), MaxConcurrentTasks: 2, MaxControlsPerTurn: 1,
}))
root := exampleBinding(definition, nil)
engine := exampleValue(agent.NewEngine(agent.EngineConfig{TreeCommitter: agent.NewMemoryTreeCommitter()}))
defer func() {
if err := engine.Close(ctx); err != nil {
panic(err)
}
}()
process := exampleValue(engine.Start(ctx, root, exampleValue(agent.EncodePayload("inspect deployment"))))
result := exampleValue(process.Await(ctx))
if err := process.Join(ctx); err != nil {
panic(err)
}
if result.Status() != agent.StatusCompleted {
panic(result.Termination().Reason())
}
output, _ := result.Output()
fmt.Println(exampleValue(output.Decode[string]()))
}
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 := agent.ParsePayload([]byte(request.Messages[0].Text()))
if err != nil {
return nil, err
}
turn, err := input.Decode[collaboration.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 collaboration.Turn) (collaboration.Decision, error) {
decision := collaboration.Decision{State: turn.State}
switch turn.Number {
case 1:
decision.Mode = collaboration.ModeContinue
decision.Tasks = []collaboration.TaskRequest{{Key: exampleValue(agent.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 = collaboration.ModeWait
reason := "No replacement instruction is needed."
decision.Controls = []collaboration.Control{{Task: turn.Tasks[0].Request.Key, CancelReason: &reason}}
case 3:
if turn.Tasks[0].Outcome == nil || turn.Tasks[0].Outcome.Result().Status() != agent.StatusCanceled {
return decision, errors.New("input cancellation did not drain")
}
decision.Mode = collaboration.ModeWait
decision.Tasks = []collaboration.TaskRequest{{Key: exampleValue(agent.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().Output()
if !present {
return decision, errors.New("review produced no output")
}
review, err := output.Decode[string]()
if err != nil {
return decision, err
}
final, err := agent.EncodePayload("coordinator continued while input was pending; " + review)
if err != nil {
return decision, err
}
decision.Mode, decision.Output = collaboration.ModeComplete, 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 agent.Definition, dispatcher agent.Dispatcher) agent.Deployment {
// These labels identify the fixed code and configuration in this checked
// example. Production bindings identify the built artifact and full config.
return exampleValue(agent.NewDeployment(agent.DeploymentConfig{
Definition: definition, Dispatcher: dispatcher,
ImplementationDigest: agent.ComputeDigest([]byte("collaboration-example-artifact")),
ConfigurationDigest: agent.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 ¶ added in v0.41.0
func (d *Definition) ChildDeployments() []agent.Deployment
ChildDeployments reports the coordinator and every worker binding.
func (*Definition) Descriptor ¶
func (d *Definition) Descriptor() agent.Descriptor
func (*Definition) Restore ¶
func (d *Definition) Restore(ctx context.Context, state agent.ExecutionState) (agent.Execution, error)
type DefinitionConfig ¶
type DefinitionConfig struct {
Name string
Description string
Coordinator WorkerConfig
Workers []WorkerConfig
StateSchema agent.Schema
OutputSchema agent.Schema
// MaxTurns is unlimited by default; a finite zero cannot admit the first turn.
MaxTurns agent.Quota
// MaxTasks is unlimited by default; a finite zero forbids worker starts.
MaxTasks agent.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 SchemaFor 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 Deployment configuration digest must cover every field; the coordinator and worker bindings enter the Deployment 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" // ModeComplete ends this collaboration and cancels unfinished descendants. ModeComplete Mode = "complete" )
type Task ¶
type Task struct {
Request TaskRequest `json:"request"`
Start *agent.ChildStartResult `json:"start,omitzero"`
Outcome *agent.ChildOutcome `json:"outcome,omitzero"`
}
Task retains one request and its canonical kernel lifecycle facts. A missing Start is pending admission; a successful Start without Outcome is outstanding.
type TaskRequest ¶
type TaskRequest struct {
Key agent.ChildKey `json:"key"`
Worker string `json:"worker"`
Input agent.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 agent.Payload `json:"state"`
Workers []agent.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 agent.Deployment
Budget agent.Budget
Capabilities agent.CapabilitySet
}
WorkerConfig freezes a child binding and its permanently allocated grants. Workers are selected by Deployment.Descriptor().Name(), never by routing or a model-supplied DeploymentRef, budget, or capability grant. The Definition owns the bound Deployments as its ChildDeployments.