collaboration

package
v0.19.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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. 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

View Source
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)

func (*Definition) Start

func (d *Definition) Start(input agent.Input) (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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL