/** * OperatorTriggerLoop - the live runtime of the trigger loop (M1-T3, extended for M2). * * A setInterval tick (NOT scheduler.addJob, which executes an agent prompt, not a callback; * precedent: connector-ingress-manual-memory-commit.ts:742) that: * 1. drains new deltas (at-least-once: commit only after processing), * 2. matches active triggers -> fires them (recall memoryQuery + surface) + recordFire, * 3. every authorEveryNTicks: the agent authors new triggers from the recent-events window, * 4. every reviewEveryNTicks: the agent reviews fired triggers (keep/refine/retire), * 5. every reportEveryNTicks: the agent composes a situational digest of the window (M2), * 6. at configured LOCAL hours: the agent composes a fuller scheduled report (M2). * * Read-only: recall/surface/log/report-to-owner only, no write-actions (M1/M2). All deps are * injected so the pipeline is unit-testable. */ import type { OperatorChannelEvent, OperatorMemoryPort, OutputSink } from './operator-interfaces.js'; import type { TriggerRecord } from './trigger-types.js'; import type { TriggerRegistry } from './trigger-registry.js'; import { type AskAgent } from './trigger-author.js'; import { type ReviewDecision } from './trigger-review.js'; import { type DeliveredFullReport } from './situation-report.js'; import type { ReportDeliveryPort } from './report-delivery-coordinator.js'; import type { ReportSchedule } from './report-scheduler.js'; import type { BackendType } from '../agent/model-runner.js'; import { type PendingReportStore } from './pending-report-store.js'; import type { ArtifactProvenance, ReportCarryTarget } from './report-carry.js'; /** Structural delta source - satisfied by ConnectorDeltaRepo. */ export interface DeltaSource { drainNew(limit: number): OperatorChannelEvent[]; commit(events: OperatorChannelEvent[]): void; } export interface TriggerLoopConfig { tickMs: number; drainLimit: number; authorEveryNTicks: number; reviewEveryNTicks: number; /** Maximum distinct newly-fired triggers reviewed in one maintenance pass. Default 1. */ reviewBatchLimit?: number; authorWindowSize: number; /** Situational-digest cadence (M1.5 + M2 output leg). Only used when deps.output is set. */ reportEveryNTicks?: number; /** * M2.4 freshness nudge debounce (ms). A poll batch that indexes new rows wakes the loop this many * ms later (Kagemusha fast-flush port). Default 15000. 0 == tick immediately on nudge. */ nudgeDebounceMs?: number; /** * Scheduled full-report suppression window (ms, default 30min): if the last * SUCCESSFUL full report (usually an on-demand one) is younger than this, * the scheduled fire skips and consumes its hour instead of sending a * near-empty duplicate. */ fullReportMinIntervalMs?: number; } export interface TriggerLoopDeps { /** Provider affects only report tool-call syntax. */ backend?: BackendType; delta: DeltaSource; memory: OperatorMemoryPort; registry: TriggerRegistry; /** Agent for structured-JSON tasks: authorTriggers (isolated JSON-only provider runtime). */ askAgent: AskAgent; /** * Agent for REPORT composition (M2.2). Bind this to the daemon's persona AgentLoop * (SOUL.md system prompt, pinned model, session continuity) - tone/quality come from the * generation inputs, and reports deserve the persona path while JSON tasks stay on the * isolated CLI. Absent -> reports use askAgent (explicit config choice, not a failure fallback). */ reportAsk?: AskAgent; /** Agent review of one trigger (real: reviewTriggerCLI). */ review: (trigger: TriggerRecord, recentContext: string[]) => Promise; /** * LEGACY owner-report sink (V2 compatibility tests only). Production * assembly wires `reportDelivery` instead; the two are mutually exclusive. */ output?: Pick & { target?: ReportCarryTarget; }; /** * TG-05/TG-06: the single owner-report delivery boundary. The loop submits * the persisted artifact and observes typed outcomes; it never calls * Telegram itself and never advances scheduler/trigger credit except on * `delivered`. */ reportDelivery?: ReportDeliveryPort; /** Owner-report target binding pending artifacts when reportDelivery is set. */ reportTarget?: ReportCarryTarget; /** Scheduled full-report cadence (real: ReportScheduler). Absent -> full leg off (M2). */ reportScheduler?: ReportSchedule; /** * M2.3: tool-call instructions for the FULL report so the agent self-gathers context. * A provider form receives the last successful report's anchor so the heavy gather * can scope its delta (`since=`); it is resolved AT FIRE TIME. */ fullReportSelfGather?: string[] | ((ctx: { lastSuccessIso: string | null; }) => string[]); /** * M8: board-reconcile feed. Invoked after commit with connector-qualified * channelKey (":") and bounded delta excerpt lines * (each carrying the event id so reconcile task writes can pass * source_event_id). Absent -> no reconcile leg. */ /** * `eventIds` is the batch itself, carried alongside the human-readable lines. * * The ids were already inside the lines as `[id:evt_...]` headers and were only ever * read back by parsing prose - which meant the SYSTEM knew the batch, flattened it to * text, and then asked the AGENT to restate it. Every change a bounded run makes rests * on this batch; carrying it is the difference between a fact and a claim. */ onChannelDelta?: (channelKey: string, lines: string[], eventIds: string[]) => void; /** * S1: durable conductor feed. Each per-channel batch is enqueued BEFORE * `delta.commit()` - a crash between the two redelivers the events and the * inbox's per-event dedupe absorbs the duplicate. Structural type, no import * cycle. Absent -> no conductor leg. */ conductorInbox?: { enqueue(batch: { channelKey: string; eventIds: string[]; lines: string[]; }): number | null; }; /** Kagemusha dual output: FULL report also publishes the operator board slots. */ fullReportBoardLines?: string[]; /** Captures the provenance of the run that has just composed a FULL report. */ fullReportProvenance?: () => ArtifactProvenance; /** Persists the exact successful FULL delivery for the later owner-turn carry. */ persistLastFullReport?: (report: DeliveredFullReport) => void; /** Durable report accumulator written before connector cursors advance. */ pendingReportStore?: PendingReportStore; config: TriggerLoopConfig; log: (line: string) => void; } export interface TickResult { tick: number; drained: number; fires: number; authored: number; reviewed: number; reported: boolean; fullReported: boolean; } interface TickOptions { /** Connector freshness ticks drain data but must not accelerate LLM maintenance passes. */ advanceMaintenance?: boolean; } export declare class OperatorTriggerLoop { private deps; private tickCount; private maintenanceTickCount; private authorWindowGeneration; private authoredWindowGeneration; private recentEvents; private running; private maintenancePending; private schedulerActive; private stopping; private activeRunPromise; private nudgeTimer; private digest; private fullReporter; private pendingDelivery; private pendingRequest; private pendingReportExpectation; private pendingReportLegacyLoaded; constructor(deps: TriggerLoopDeps); /** Reloads one durable outcome so recovery can hydrate this live loop before report work. */ private refreshPendingReportState; private isPendingReportWorkBlocked; private persistPendingReports; private reporterFor; /** A report sink exists: either the coordinator port or the legacy test sink. */ private hasReportSink; private deliveryIdFor; private requireOutputTarget; private assertPendingTarget; private assertPendingDeliveryBinding; private assertPendingRequestBinding; private deliverPendingReport; private markScheduledFullOutcome; private prepareAndDeliverReport; private preparePendingRequest; private recoverPendingReportWork; tick(options?: TickOptions): Promise; /** * M2.4 freshness nudge: wake the loop to tick ~nudgeDebounceMs from now instead of waiting for the * next scheduled interval. The connector sink calls this (via a forwarder) whenever a poll batch * indexes new rows. * * Debounced (Kagemusha fast-flush port, agent-awareness.ts:322-332 mechanism): the FIRST nudge in * a quiet window arms one timer; further nudges while it is armed are ignored, so a burst of poll * batches collapses to a single extra tick. Busy-safe (agent-awareness.ts:343-346 mechanism): if a * tick is in flight when the timer fires, the nudge is skipped - never concurrent ticks; the * uncommitted deltas simply wait for the next tick. The extra tick drains and reports, but does * not advance author/review maintenance cadence. */ /** * On-demand full report (plan v6 S1-T3): the owner's "give me the full * report" intent routed to the SAME machinery as the scheduled leg - same * reporter, same anchor semantics, same serial guard. Host-code entry * (gateway forwarder hook); the run itself is fire-and-forget so the chat * turn that triggered it is never blocked (and never nests lane runs). * * Consume semantics: success marks the current hourKey fired, so a * scheduled fire in the same hour does not duplicate; markSuccess advances * the delta anchor exactly like a scheduled run. */ startFullReport(): { accepted: boolean; reason?: 'busy' | 'unavailable'; }; nudge(): void; /** * Start ticking on the configured interval. Returns a stop function. * The interval wrapper catches + logs tick errors so one bad tick does not kill the loop * (the error is still surfaced loudly in the log - not swallowed). */ start(): () => Promise; private launchRun; private finishRun; } export {}; //# sourceMappingURL=operator-trigger-loop.d.ts.map