/** * Working Memory Service — auto-injectable "why am I awake" wake card * for agent prompts. * * Builds a compact (≤1.5KB) markdown card that summarizes the most- * recent active WorkItem assigned to an agent, the parent Request, the * upstream WorkItem chain (max 5), the required next action, and any * known constraints / open loops on the Request — so every agent wakes * with the answer to "why am I awake right now?" without having to * query the task pool manually. * * Per Memory Phase 1 / Fix M2: today, when an agent wakes (assigned * to a WorkItem), it doesn't automatically know its parent Request, * the upstream task chain, or relevant prior episodes — it receives * just the WorkItem description and has to query manually. This * service closes that gap by feeding a tiny wake card into prompt * assembly automatically, alongside the M1 mission card. * * Out of scope for Phase 1 (deferred to later phases): * - Narrative summarization of upstream chain (no LLM round-trip) * - Episode memory / event timeline (M5 work) * - Vector embedding / semantic recall * - Memory review queue / supersession * * @module services/memory/working-memory.service */ import type { WorkItem, WorkItemStatus } from '../../types/v2/work-item.types.js'; import type { Request } from '../../types/v2/request.types.js'; /** * Hard upper bound on the rendered wake card. Spec calls for ≤1.5KB; * we cap at 1.5KB and truncate aggressively (drop optional sections * first, then trim verbose sections) when over budget. */ export declare const WAKE_CARD_HARD_CAP_BYTES = 1536; /** * WorkItem statuses that count as "currently active for this agent". * * An agent that is `proposed` an item is about to wake on it, an * `accepted` / `running` agent is mid-flight, and `blocked` / * `escalated` are still the agent's open loop. Terminal statuses * (done, verified, failed, cancelled) are NOT counted — the agent * has nothing to wake on if its only assignment is closed. */ declare const ACTIVE_WORK_ITEM_STATUSES: ReadonlySet; /** * Inputs accepted by `getWakeCardForAgent`. * * `sessionName` is what the WorkItem.target field is set to (e.g. * `crewly-product-leo-21a5477e`); the service uses this to find the * agent's currently-active WorkItem. `agentId` is used for logging * only (typically the same string, but we keep them separate so a * caller using memberId for logging can still set sessionName for * the lookup). */ export interface WakeCardRequest { /** Agent identifier — used for logging only. */ agentId: string; /** Agent's session name — used to filter WorkItems by target. */ sessionName: string; /** Absolute path to the project root that owns `.crewly/`. */ projectPath: string; /** Optional team id — currently unused, reserved for future filters. */ teamId?: string; } /** * Internal shape used during card rendering. Captures the data points * the spec calls out and lets `renderWakeCard` stay pure / testable. */ export interface WakeCardData { /** The current active WorkItem for this agent, if any. */ current: WorkItem | null; /** The parent Request, if linked. */ parentRequest: Request | null; /** * The upstream WorkItem chain (oldest → most-recent), up to * UPSTREAM_CHAIN_MAX_ITEMS entries. Excludes `current` itself. */ upstreamChain: WorkItem[]; /** * Open-loop / constraint summaries surfaced under "Known constraints". * Sourced from Request metadata when present. */ openLoops: string[]; } /** * Service that builds the working-memory wake card injected into * agent prompts. * * Singleton (mirrors {@link MissionContextService}). Caller side is * fail-soft: any read error logs a warning and the service returns * the empty string so prompt assembly never breaks. * * @example * ```typescript * const svc = WorkingMemoryService.getInstance(); * const card = await svc.getWakeCardForAgent({ * agentId: 'member-uuid', * sessionName: 'crewly-product-leo-21a5477e', * projectPath: '/Users/me/projects/crewly', * teamId: '28a4ac10-...', * }); * if (card) prompt += `\n${card}`; * ``` */ export declare class WorkingMemoryService { private static instance; private readonly logger; private constructor(); /** Get the shared singleton instance. */ static getInstance(): WorkingMemoryService; /** Reset for tests. */ static clearInstance(): void; /** * Build the working-memory wake card for an agent. * * Returns the empty string when no signals are available (no active * WorkItem assigned to this agent) — callers should treat empty * output as "skip the section entirely" rather than rendering an * empty heading. * * Errors reading the task pool / request store are caught and logged * as warnings; the method still returns the empty string so prompt * assembly never breaks. * * @param req - Request inputs * @returns A ≤1.5KB markdown card, or `''` when nothing is worth showing */ getWakeCardForAgent(req: WakeCardRequest): Promise; /** * Gathers everything the renderer needs for one agent. * * Steps: * 1. Find the agent's most-recent active WorkItem in the task pool. * 2. If linked, load the parent Request. * 3. Walk the parentWorkItemId chain (most-recent → oldest), up to * UPSTREAM_CHAIN_MAX_ITEMS, and reverse for chronological order. * 4. Extract open-loop / constraint summaries from Request metadata. * * @param req - Caller request * @returns Populated WakeCardData (current may be null when nothing * is awake for this agent). */ private collectWakeData; /** * Loads a Request by id, returning null on miss / error. * * Wraps {@link RequestService.getById} (singleton scoped to * `req.projectPath`) so a missing or malformed Request file is * downgraded to a debug-level warning rather than aborting the * whole wake-card build. */ private loadRequest; } /** * Pick the most-recent active WorkItem assigned to an agent. * * "Active" = status in {@link ACTIVE_WORK_ITEM_STATUSES}. "Most recent" * = greatest `startedAt` (when set) else greatest `createdAt`. Ties * resolve to whichever item appears later in the iteration order * (effectively pool insertion order). * * Returns null when no item is currently active for this agent. * * @param items - All WorkItems in the pool * @param sessionName - The agent's session name (matched against `target`) */ export declare function pickCurrentWorkItem(items: readonly WorkItem[], sessionName: string): WorkItem | null; /** * Walk the `parentWorkItemId` chain to build the upstream task * sequence in chronological order (oldest → most-recent), capped at * `maxItems` entries. The current WorkItem itself is NOT included in * the result. * * Defends against cycles by tracking visited ids. * * @param items - All WorkItems in the pool * @param current - The wake-card's current WorkItem (chain anchor) * @param maxItems - Maximum number of upstream entries to surface */ export declare function buildUpstreamChain(items: readonly WorkItem[], current: WorkItem, maxItems: number): WorkItem[]; /** * Extract open-loop / constraint summaries from a parent Request. * * Today the Request type does not have a dedicated `openLoops` field, * so we derive one signal: the `confirmationReason` (when set — * indicates a pending user confirmation that the agent should be * aware of). When neither signal is present, returns an empty array * and the renderer drops the section entirely. * * Phase 2 (M5 / M6) will add richer signals (decisions, lessons, * outstanding sub-task statuses); this function is the extension * point. * * @param request - Parent Request, or null */ export declare function collectOpenLoops(request: Request | null): string[]; /** * Render a one-line summary for a WorkItem suitable for the * upstream-chain bullet list. Format: `" · "` truncated * at SUMMARY_LINE_MAX_CHARS. */ export declare function summarizeWorkItem(wi: WorkItem): string; /** * Compose the full wake-card markdown from collected data. * * Sections (in order): * 1. Header — "## Why You Are Awake" * 2. WorkItem — title (always present when called) * 3. Parent Request — title + 1-line summary * 4. Upstream chain — bullet list of prior WorkItems (max 5) * 5. Required next action — from WorkItem description * 6. Known constraints / open loops — from Request metadata (if any) * * Sections 3-6 are emitted only when they have content; an absent * parent Request or empty chain simply drops the corresponding * heading. */ export declare function renderWakeCard(data: WakeCardData): string; /** * Enforce the hard byte cap. Truncates the card at the last newline * boundary that still fits, so we never cut a markdown bullet * mid-line. If even the header exceeds the cap (shouldn't happen), * the input is hard-truncated by byte length. * * @param content - Original rendered card * @param capBytes - Hard cap in UTF-8 bytes * @returns The card, possibly truncated, never exceeding `capBytes` */ export declare function enforceHardCap(content: string, capBytes: number): string; /** Re-export for tests. */ export { ACTIVE_WORK_ITEM_STATUSES }; //# sourceMappingURL=working-memory.service.d.ts.map