ca243a2d50
executus CI / test (push) Failing after 24s
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>
87 lines
3.7 KiB
Go
87 lines
3.7 KiB
Go
package run
|
|
|
|
import "context"
|
|
|
|
// ProgressSink reports a one-line progress note for the current run upward to
|
|
// any ancestor run that is being watched by a run-critic. It exists to solve a
|
|
// specific false-positive: when an agent calls a long-running skill/agent as a
|
|
// single tool, the parent's agent loop is BLOCKED on that one tool call for the
|
|
// whole child run, so the parent's progress recorder sees "zero iterations,
|
|
// zero new tokens, no activity" and its critic concludes the tool "hung
|
|
// indefinitely" — even though the child is iterating happily. Forwarding the
|
|
// child's per-step activity up the chain keeps every blocked ancestor's
|
|
// last-activity fresh, so a healthy-but-slow child is no longer mistaken for a
|
|
// hang. A nil ProgressSink is safe to ignore (there is no ancestor to notify).
|
|
type ProgressSink func(note string)
|
|
|
|
type progressSinkKey struct{}
|
|
|
|
// WithProgressSink returns a context carrying sink for descendant runs to find
|
|
// via ProgressSinkFrom. A nil sink is stored as-is (ProgressSinkFrom returns
|
|
// nil), which callers treat as "no ancestor watching".
|
|
func WithProgressSink(ctx context.Context, sink ProgressSink) context.Context {
|
|
return context.WithValue(ctx, progressSinkKey{}, sink)
|
|
}
|
|
|
|
// ProgressSinkFrom returns the ancestor progress sink carried on ctx, or nil
|
|
// if none is wired. The returned sink, when non-nil, forwards a note to the
|
|
// immediate parent run's recorder AND (transitively) to every further
|
|
// ancestor, because each level installs a sink that forwards upward.
|
|
func ProgressSinkFrom(ctx context.Context) ProgressSink {
|
|
if v := ctx.Value(progressSinkKey{}); v != nil {
|
|
if s, ok := v.(ProgressSink); ok {
|
|
return s
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InstallProgressBridge wires the current run into the ancestor progress chain.
|
|
//
|
|
// report — this run's own recorder hook (e.g. recorder.OnStatus). nil when
|
|
// the run has no critic recorder of its own (the common skill case);
|
|
// the bridge then purely forwards descendants' progress upward.
|
|
//
|
|
// It returns:
|
|
//
|
|
// childCtx — pass this to the agent loop / toolbox so descendant runs
|
|
// (invoked as tools) forward their progress into this chain.
|
|
// notifyAncestors — call this on each of THIS run's own loop steps to keep
|
|
// every ancestor critic's last-activity fresh. nil when this
|
|
// run has no ancestors (it is a top-level run); nil-safe to
|
|
// call only via the returned value being checked, so callers
|
|
// should guard `if notifyAncestors != nil`.
|
|
//
|
|
// The chain is built so that a note from any descendant bumps the recorders of
|
|
// ALL of its blocked ancestors, not just its immediate parent.
|
|
func InstallProgressBridge(ctx context.Context, report ProgressSink) (childCtx context.Context, notifyAncestors ProgressSink) {
|
|
parent := ProgressSinkFrom(ctx)
|
|
|
|
// The sink descendants will call. It must bump this run's own recorder
|
|
// (report) AND forward to all ancestors (parent). Collapse to the minimal
|
|
// closure so we don't stack a no-op wrapper for recorder-less runs.
|
|
var child ProgressSink
|
|
switch {
|
|
case report == nil:
|
|
// No recorder of our own: descendants forward straight to ancestors.
|
|
child = parent
|
|
case parent == nil:
|
|
// Top-level run with a recorder: descendants feed only our recorder.
|
|
child = report
|
|
default:
|
|
child = func(note string) {
|
|
report(note)
|
|
parent(note)
|
|
}
|
|
}
|
|
|
|
childCtx = ctx
|
|
if child != nil {
|
|
childCtx = WithProgressSink(ctx, child)
|
|
}
|
|
// This run's own steps notify ancestors directly (its own recorder is fed
|
|
// separately by its step observer, so we deliberately do not call report
|
|
// here — only the ancestors need waking).
|
|
return childCtx, parent
|
|
}
|