@tangle-network/agent-runtime / analyst-loop
Knowledge-side bridge — consumers wire proposeFromFindings from agent-knowledge.
TProposal = unknown
proposeFromFindings(
findings):KnowledgeProposalBatch<TProposal> |Promise<KnowledgeProposalBatch<TProposal>>
Convert a findings batch into proposals. Returns the partitioned
result so the loop can report malformed
findings. Implementations SHOULD honour the convention "non-
knowledge subjects return null and are counted in skipped."
readonly AnalystFinding[]
KnowledgeProposalBatch<TProposal> | Promise<KnowledgeProposalBatch<TProposal>>
TProposal = unknown
proposals:
TProposal[]
skipped:
number
errors:
object[]
findingId:
string
subject:
string
message:
string
Agent-surface bridge — proposes prompt, skill, tool, and scaffolding edits.
TEdit = unknown
proposeFromFindings(
findings):ImprovementEditBatch<TEdit> |Promise<ImprovementEditBatch<TEdit>>
readonly AnalystFinding[]
ImprovementEditBatch<TEdit> | Promise<ImprovementEditBatch<TEdit>>
TEdit = unknown
edits:
TEdit[]
skipped:
number
errors:
object[]
findingId:
string
subject:
string
message:
string
runId:
string
The run id of the work being analysed.
registry:
AnalystRegistryLike
The registry — pre-populated with the analyst kinds the consumer wants.
inputs:
AnalystRunInputs
Inputs forwarded to registry.run — typically { traceStore }.
findingsStore:
FindingsStoreLike|null
Findings ledger. The loop appends the new run + diffs against the
baseline run before running adapters. Pass null to skip
persistence (useful for one-shot analyses).
optionalbaselineRunId?:string|null
Prior run id whose findings the loop reads + provides to analysts
as priorFindings AND diffs against. When omitted, the loop picks
the most recent run in the store (excluding runId itself); pass
null to explicitly start with an empty baseline.
optionalpriorFindingsStrategy?:"none"|"per-kind"|"wildcard"
Strategy for forwarding prior findings into ctx.priorFindings.
optionalchainFindings?:boolean
Pass findings produced earlier in this registry run to each later analyst
through ctx.upstreamFindings.
Registration order becomes dependency order when enabled.
Disabled by default so independent analyst suites keep their current behavior.
optionalknowledgeProposalSource?:KnowledgeProposalSource<unknown>
Knowledge-side bridge — usually agent-knowledge's proposeFromFindings.
optionalimprovementProposalSource?:ImprovementProposalSource<unknown>
Agent-surface bridge — usually a prompt, skill, or tool diff producer.
optionalcostLedger?:CostLedgerHandle
Shared account for analyst calls that belong to a larger improvement run.
optionalcostPhase?:string
Attribution label forwarded to every analyst call in the shared account.
optionalsignal?:AbortSignal
Cancels analyst work before downstream proposal work starts.
optionallog?: (msg,fields?) =>void
Optional logger. Defaults to console.log for [analyst-loop] lines.
string
Record<string, unknown>
void
optionalonEvent?: (event) =>void|Promise<void>
Event sink for live progress. Called for every phase of the loop:
baseline resolution, registry events forwarded from runStream,
ledger persistence, diff, knowledge / improvement proposals, and
the terminal loop-completed. Awaited so
slow sinks (SSE write, JSONL append) apply backpressure.
The callback MUST NOT throw — exceptions propagate and abort the loop. Catch + swallow internally if your sink is unreliable.
void | Promise<void>
TProposal = unknown
TEdit = unknown
runId:
string
baselineRunId:
string|null
durationMs:
number
Full wall-clock time for analysis, persistence, and proposal preparation.
analystResult:
AnalystRunResult
diff:
FindingsDiff|null
knowledge:
KnowledgeReport<TProposal> |null
improvement:
ImprovementReport<TEdit> |null
TProposal = unknown
proposals:
TProposal[]
skipped:
number
errors:
object[]
findingId:
string
subject:
string
message:
string
TEdit = unknown
edits:
TEdit[]
skipped:
number
errors:
object[]
findingId:
string
subject:
string
message:
string
Narrowed shape we accept for AnalystRegistry so the orchestrator
remains testable without instantiating the real class. The real
class satisfies this trivially.
list(): readonly
object[]
readonly object[]
run(
runId,inputs,opts?):Promise<AnalystRunResult>
string
AnalystRunInputs
readonly AnalystFinding[] | Record<string, readonly AnalystFinding[]>
boolean
Promise<AnalystRunResult>
Narrowed shape we accept for FindingsStore.
loadAll(): readonly
AnalystFinding&object[]
readonly AnalystFinding & object[]
loadRun(
runId): readonlyAnalystFinding&object[]
string
readonly AnalystFinding & object[]
append(
runId,findings):Promise<void>
string
readonly AnalystFinding[]
Promise<void>
Narrow the AnalystRegistryLike further when we need streaming: the
loop checks if the registry exposes runStream and uses it when
present, falling back to run() otherwise. This keeps the type
surface backwards-compatible — older registry shims that only
implement run still work; they just don't forward per-analyst
events.
list(): readonly
object[]
readonly object[]
run(
runId,inputs,opts?):Promise<AnalystRunResult>
string
AnalystRunInputs
readonly AnalystFinding[] | Record<string, readonly AnalystFinding[]>
boolean
Promise<AnalystRunResult>
optionalrunStream(runId,inputs,opts?):AsyncIterable<AnalystRunEvent>
string
AnalystRunInputs
readonly AnalystFinding[] | Record<string, readonly AnalystFinding[]>
boolean
AsyncIterable<AnalystRunEvent>
AnalystLoopEvent = {
type:"baseline-resolved";runId:string;baselineRunId:string|null;priorFindingCount:number; } | {type:"analyst";runId:string;event:AnalystRunEvent; } | {type:"findings-persisted";runId:string;count:number; } | {type:"diff-computed";runId:string;baselineRunId:string;appeared:number;disappeared:number;persisted:number;changed:number; } | {type:"knowledge-proposed";runId:string;proposalCount:number;skipped:number;errors:number; } | {type:"improvement-proposed";runId:string;editCount:number;skipped:number;errors:number; } | {type:"loop-completed";runId:string;durationMs:number; }
Events emitted by runAnalystLoop via opts.onEvent. UIs and
JSONL tail-sinks consume this stream. The loop awaits each
callback so a slow sink applies backpressure to the loop's phases
(e.g. an SSE write that takes 200ms delays the next phase by
200ms — the loop never out-paces its observer).
Forwards registry events verbatim via analyst so consumers don't
have to wire two streams.
{ type: "baseline-resolved"; runId: string; baselineRunId: string | null; priorFindingCount: number; }
{ type: "analyst"; runId: string; event: AnalystRunEvent; }
type:
"analyst"
runId:
string
event:
AnalystRunEvent
Forwarded verbatim from AnalystRegistry.runStream.
{ type: "findings-persisted"; runId: string; count: number; }
{ type: "diff-computed"; runId: string; baselineRunId: string; appeared: number; disappeared: number; persisted: number; changed: number; }
{ type: "knowledge-proposed"; runId: string; proposalCount: number; skipped: number; errors: number; }
{ type: "improvement-proposed"; runId: string; editCount: number; skipped: number; errors: number; }
{ type: "loop-completed"; runId: string; durationMs: number; }
iterationsToTraceStore<
Task,Output>(iterations,budgets?):TraceAnalysisStore
Build an in-memory TraceAnalysisStore over a loop round's iterations. Fail-loud on an
empty round — there is nothing for an analyst to read, and a silent empty store would
mask a broken capture path.
Task
Output
readonly Iteration<Task, Output>[]
TraceAnalystByteBudgets = DEFAULT_TRACE_ANALYST_BUDGETS
TraceAnalysisStore
runAnalystLoop<
TProposal,TEdit>(opts):Promise<RunAnalystLoopResult<TProposal,TEdit>>
Analyze a run and apply accepted knowledge and agent-surface proposals.
TProposal = unknown
TEdit = unknown
Promise<RunAnalystLoopResult<TProposal, TEdit>>