Spaces:
Sleeping
Sleeping
| /** | |
| * state.ts β DeepTrust Research Agent | |
| * | |
| * Single source of truth for every field that flows through the | |
| * StateGraph. Zod gives us runtime validation; the inferred TS | |
| * types keep every node strictly typed with zero `any` leakage. | |
| */ | |
| import { z } from "zod"; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Enumerations | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Every legal node name in the graph β used to type routing returns. */ | |
| export const NodeName = z.enum([ | |
| "thinker", | |
| "auditor", | |
| "tool_executor", | |
| "synthesizer", | |
| "__end__", | |
| ]); | |
| export type NodeName = z.infer<typeof NodeName>; | |
| /** Disposition returned by the Auditor node. */ | |
| export const AuditVerdict = z.enum(["approved", "rejected", "needs_revision"]); | |
| export type AuditVerdict = z.infer<typeof AuditVerdict>; | |
| /** Lifecycle of a single research run. */ | |
| export const RunStatus = z.enum([ | |
| "idle", | |
| "thinking", | |
| "awaiting_approval", // HITL interrupt point | |
| "executing", | |
| "synthesizing", | |
| "complete", | |
| "failed", | |
| ]); | |
| export type RunStatus = z.infer<typeof RunStatus>; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Sub-schemas | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * A single step inside a research plan. | |
| * The Thinker produces an array of these; the Tool Executor | |
| * works through them one-by-one. | |
| */ | |
| export const ResearchStep = z.object({ | |
| id: z.string().uuid(), | |
| tool: z.enum(["web_search"]), | |
| input: z.string().min(1), | |
| rationale: z.string(), | |
| /** Populated by the Tool Executor after the step runs. */ | |
| output: z.string().optional(), | |
| executedAt: z.string().datetime().optional(), | |
| }); | |
| export type ResearchStep = z.infer<typeof ResearchStep>; | |
| /** | |
| * The structured plan produced by the Thinker and evaluated | |
| * by the Auditor before any tool is touched. | |
| */ | |
| export const ResearchPlan = z.object({ | |
| objective: z.string().min(1), | |
| steps: z.array(ResearchStep).min(1).max(6), | |
| estimatedTokenBudget: z.number().int().positive(), | |
| createdAt: z.string().datetime(), | |
| revision: z.number().int().nonnegative().default(0), | |
| }); | |
| export type ResearchPlan = z.infer<typeof ResearchPlan>; | |
| /** | |
| * One entry in the reasoning trail β every node appends here | |
| * so the UI can replay the full thought process. | |
| */ | |
| export const ReasoningEntry = z.object({ | |
| node: NodeName, | |
| timestamp: z.string().datetime(), | |
| summary: z.string(), | |
| /** Optional verbatim model output for deep-dive inspection. */ | |
| rawThought: z.string().optional(), | |
| }); | |
| export type ReasoningEntry = z.infer<typeof ReasoningEntry>; | |
| /** Structured feedback from the Auditor when it rejects a plan. */ | |
| export const AuditResult = z.object({ | |
| verdict: AuditVerdict, | |
| policyViolations: z.array(z.string()).default([]), | |
| suggestions: z.array(z.string()).default([]), | |
| auditedAt: z.string().datetime(), | |
| }); | |
| export type AuditResult = z.infer<typeof AuditResult>; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Root State Schema | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * ResearchState β the complete, checkpointable state object. | |
| * | |
| * LangGraph.js will serialise/deserialise this on every step, | |
| * so every field must be JSON-safe. The checkpointer stores | |
| * snapshots keyed by `threadId`, enabling full rehydration. | |
| */ | |
| export const ResearchState = z.object({ | |
| // ββ Identity & Persistence βββββββββββββββββββββββββββββββ | |
| /** Stable identifier β passed to the checkpointer as the config key. */ | |
| threadId: z.string().uuid(), | |
| /** Human-readable label shown in the UI. */ | |
| sessionName: z.string().default("Unnamed Session"), | |
| // ββ Input ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** The raw research question submitted by the user. */ | |
| userQuery: z.string().min(1), | |
| /** | |
| * Retrieved context from the user's local knowledge base (client-side RAG). | |
| * Passed into Thinker and Synthesizer so the LLM can use it when planning and reporting. | |
| */ | |
| knowledgeContext: z.string().optional(), | |
| /** | |
| * URLs the user added as references. Thinker can emit document_fetch steps for these. | |
| */ | |
| contextUrls: z.array(z.string()).optional(), | |
| // ββ Planning βββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Current plan produced by the Thinker. Null before first plan. */ | |
| plan: ResearchPlan.nullable().default(null), | |
| /** | |
| * Feedback injected into the Thinker's context when the Auditor | |
| * rejects a plan. Cleared after each successful audit. | |
| */ | |
| rejectionFeedback: z.string().nullable().default(null), | |
| /** How many times the plan has cycled through ThinkerβAuditor. */ | |
| planRevisionCount: z.number().int().nonnegative().default(0), | |
| /** Safety ceiling β prevents runaway revision loops. */ | |
| maxPlanRevisions: z.number().int().positive().default(5), | |
| // ββ Auditing βββββββββββββββββββββββββββββββββββββββββββββ | |
| /** The most recent audit result. Null before first audit. */ | |
| auditResult: AuditResult.nullable().default(null), | |
| // ββ Execution ββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Index into `plan.steps` for the currently-executing step. */ | |
| currentStepIndex: z.number().int().nonnegative().default(0), | |
| /** | |
| * Whether the user has explicitly approved the plan. | |
| * The graph checks this before transitioning to tool_executor. | |
| */ | |
| humanApproved: z.boolean().default(false), | |
| // ββ Output βββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Final synthesized report. Null until synthesizer runs. */ | |
| finalReport: z.string().nullable().default(null), | |
| // ββ Observability ββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Append-only log of every node's reasoning. | |
| * The UI streams this list to render a live "thought process" view. | |
| */ | |
| reasoning: z.array(ReasoningEntry).default([]), | |
| // ββ Lifecycle ββββββββββββββββββββββββββββββββββββββββββββ | |
| status: RunStatus.default("idle"), | |
| /** ISO timestamp of the last state mutation. */ | |
| updatedAt: z.string().datetime(), | |
| /** Non-null when the run terminated with an unrecoverable error. */ | |
| errorMessage: z.string().nullable().default(null), | |
| }); | |
| export type ResearchState = z.infer<typeof ResearchState>; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Helpers | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const MAX_REASONING_ENTRIES = 20; | |
| /** | |
| * Create a single-element array with the new reasoning entry. | |
| * The channel reducer in graph.ts handles concatenation and capping. | |
| */ | |
| export function appendReasoning( | |
| _state: ResearchState, | |
| entry: Omit<ReasoningEntry, "timestamp"> | |
| ): ReasoningEntry[] { | |
| return [{ ...entry, timestamp: new Date().toISOString() }]; | |
| } | |
| /** Returns a fresh, validated initial state for a new session. */ | |
| export function createInitialState( | |
| params: Pick< | |
| ResearchState, | |
| "threadId" | "userQuery" | "sessionName" | "knowledgeContext" | "contextUrls" | |
| > | |
| ): ResearchState { | |
| return ResearchState.parse({ | |
| ...params, | |
| updatedAt: new Date().toISOString(), | |
| }); | |
| } | |