Documentation
¶
Overview ¶
Package collaboration provides bounded, 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 freezes only child references, descriptors, budgets, and capabilities; the Engine resolves the executable Deployments. The root Descriptor owns the carried-state and final-output schemas.
Continue runs another coordinator turn while workers remain active. Wait 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. 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. A rejected worker start counts toward MaxTasks. Admitted tasks count toward MaxConcurrentTasks until their drained outcomes are observed. MaxTurns and MaxControlsPerTurn bound coordinator decisions and each control batch.
Controls compile to agent.SignalChild and agent.CancelChild. 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.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidConfig = errors.New("collaboration: invalid configuration") ErrInvalidDecision = errors.New("collaboration: invalid decision") ErrInvalidState = errors.New("collaboration: invalid execution state") ErrInvalidProtocol = errors.New("collaboration: invalid execution protocol") ErrTurnLimit = errors.New("collaboration: turn limit reached") )
Functions ¶
This section is empty.
Types ¶
type Control ¶
type Control struct {
Task agent.ChildKey `json:"task"`
Signal *agent.SignalRequest `json:"signal,omitempty"`
CancelReason *string `json:"cancel_reason,omitempty"`
}
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,omitempty"`
}
ControlReceipt preserves both the declared action and its admission result.
type Decision ¶
type Decision struct {
Mode Mode `json:"mode"`
State agent.Input `json:"state"`
Tasks []TaskRequest `json:"tasks,omitempty"`
Controls []Control `json:"controls,omitempty"`
Output *agent.Output `json:"output,omitempty"`
}
Decision is the coordinator's output contract. The entire batch is validated before any action is declared. Complete requires Output and no actions; other modes prohibit 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"
"encoding/json"
"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: 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: 32, Effects: 16, Signals: 32}
render := exampleValue(workflow.Transform("render_turn", func(_ context.Context, turn collaboration.Turn) (interaction.Input, error) {
payload, err := json.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.ParseOutput([]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, model.DeploymentRef())
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: 64, Effects: 32, Signals: 64}},
Workers: []collaboration.WorkerConfig{{Deployment: gate, Budget: budget}, {Deployment: worker, Budget: budget}},
StateSchema: textSchema, OutputSchema: textSchema, MaxTurns: 4, MaxTasks: 2, MaxConcurrentTasks: 2, MaxControlsPerTurn: 1,
}))
root := exampleBinding(definition, nil, coordinator.DeploymentRef(), gate.DeploymentRef(), worker.DeploymentRef())
engine := exampleValue(agent.NewEngine(agent.EngineConfig{DeploymentResolver: exampleResolver{
model.DeploymentRef(): model, coordinator.DeploymentRef(): coordinator,
gate.DeploymentRef(): gate, worker.DeploymentRef(): worker,
}}))
defer func() {
if err := engine.Close(ctx); err != nil {
panic(err)
}
}()
process := exampleValue(engine.Start(ctx, root, exampleValue(agent.EncodeInput("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.ParseInput([]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 := json.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.Continue
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.Wait
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.Wait
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.EncodeOutput("coordinator continued while input was pending; " + review)
if err != nil {
return decision, err
}
decision.Mode, decision.Output = collaboration.Complete, &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, children ...agent.DeploymentRef) agent.Deployment {
// These labels identify the fixed code and configuration in this checked
// example. Production bindings identify the built artifact and full config.
configuration := struct {
Name string
Children []agent.DeploymentRef
}{definition.Descriptor().Name(), children}
return exampleValue(agent.NewDeployment(agent.DeploymentConfig{
Definition: definition, Dispatcher: dispatcher,
ImplementationDigest: agent.ComputeDigest([]byte("collaboration-example-artifact")),
ConfigurationDigest: agent.ComputeDigest(exampleValue(json.Marshal(configuration))),
}))
}
type exampleResolver map[agent.DeploymentRef]agent.Deployment
func (e exampleResolver) Resolve(reference agent.DeploymentRef) (agent.Deployment, error) {
if deployment, found := e[reference]; found {
return deployment, nil
}
return agent.Deployment{}, errors.New("exact deployment unavailable")
}
Output: coordinator continued while input was pending; reviewed: inspect deployment
func NewDefinition ¶
func NewDefinition(config DefinitionConfig) (*Definition, error)
func (*Definition) Descriptor ¶
func (d *Definition) Descriptor() agent.Descriptor
func (*Definition) Restore ¶
func (d *Definition) Restore(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 uint32
MaxTasks uint32
MaxConcurrentTasks uint32
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 and child binding.
type Mode ¶
type Mode string
Mode chooses when the next coordinator turn runs.
const ( // Continue starts the next turn after action receipts, while tasks run. Continue Mode = "continue" // Wait starts the next turn after at least one outstanding task drains. // If every attempted start failed, their receipts trigger the next turn. Wait Mode = "wait" // Complete ends this collaboration and cancels unfinished descendants. Complete Mode = "complete" )
type Task ¶
type Task struct {
Request TaskRequest `json:"request"`
Start *agent.ChildStartResult `json:"start,omitempty"`
Outcome *agent.ChildOutcome `json:"outcome,omitempty"`
}
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.Input `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 uint32 `json:"number"`
State agent.Input `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 retains only immutable references, contracts, and grants; the Engine's resolver owns the executable Deployments.