Batteries-included agent-harness base, extracted from mort's agent layer. This first cut establishes the module + the zero-coupling core primitives: - lane, dispatchguard, pendingattach, run/progress.go: moved verbatim from mort - config: host config Source seam + env-var default (nil-safe helpers) - deliver: output-egress seam + Discard/Stdout defaults - identity: AdminPolicy + MemberResolver seams (nil-safe) - fanout: programmatic N×M swarm (bounded global + per-key concurrency) - README/CLAUDE.md with the vibe-coded banner; CI with Go gates + the "core stays majordomo+stdlib only" invariant Core builds with stdlib only today; majordomo enters at P1 (model/structured). go build/vet/test -race all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
// Package deliver is executus's output-egress seam.
|
||||
//
|
||||
// Where a run's final output and any generated artifacts go is host-specific:
|
||||
// Mort posts a Discord embed (with a paste fallback and state-react emoji),
|
||||
// Gadfly consolidates findings into one PR comment, a CLI host prints to stdout.
|
||||
// The harness depends only on the Delivery interface and ships two defaults so a
|
||||
// new host needs no wiring: Discard (return output to the caller only) and
|
||||
// Stdout.
|
||||
package deliver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Target names where output should land. Its fields are host-interpreted (a
|
||||
// Discord channel ID, a PR number, etc.); the harness never parses them.
|
||||
type Target struct {
|
||||
Kind string // host-defined: "channel", "dm", "thread", "stdout", "comment", ...
|
||||
ID string
|
||||
}
|
||||
|
||||
// Artifact is a generated file accompanying a run's output (an image, a report,
|
||||
// a STL, ...). Bytes are owned by the caller; a Delivery must not retain them
|
||||
// past the call without copying.
|
||||
type Artifact struct {
|
||||
Name string
|
||||
MIME string
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// Delivery is the output seam. Deliver returns a host-defined id for the posted
|
||||
// output when one exists (a message ID, a paste URL); an empty id is fine.
|
||||
// Implementations must be nil-safe for a nil/empty artifacts slice.
|
||||
type Delivery interface {
|
||||
Deliver(ctx context.Context, t Target, output string, artifacts []Artifact) (id string, err error)
|
||||
DeliverError(ctx context.Context, t Target, runErr error) error
|
||||
}
|
||||
|
||||
// Discard is the light-host default: it drops the output (the caller already has
|
||||
// it as the run Result). Gadfly uses this — it reads results in-process and does
|
||||
// its own consolidation.
|
||||
type Discard struct{}
|
||||
|
||||
func (Discard) Deliver(context.Context, Target, string, []Artifact) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (Discard) DeliverError(context.Context, Target, error) error { return nil }
|
||||
|
||||
// Stdout writes output (and an artifact manifest) to an io.Writer, defaulting to
|
||||
// os.Stdout. Handy for local/dev and example hosts.
|
||||
type Stdout struct{ W io.Writer }
|
||||
|
||||
func (s Stdout) w() io.Writer {
|
||||
if s.W != nil {
|
||||
return s.W
|
||||
}
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
func (s Stdout) Deliver(_ context.Context, t Target, output string, artifacts []Artifact) (string, error) {
|
||||
w := s.w()
|
||||
if t.Kind != "" || t.ID != "" {
|
||||
fmt.Fprintf(w, "[%s:%s]\n", t.Kind, t.ID)
|
||||
}
|
||||
fmt.Fprintln(w, output)
|
||||
for _, a := range artifacts {
|
||||
fmt.Fprintf(w, " <artifact %s (%s, %d bytes)>\n", a.Name, a.MIME, len(a.Bytes))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s Stdout) DeliverError(_ context.Context, _ Target, runErr error) error {
|
||||
fmt.Fprintf(s.w(), "ERROR: %v\n", runErr)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user