reasonix 0.0.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { WriteStream } from 'node:fs';
2
+
1
3
  /**
2
4
  * Retry layer for DeepSeek API calls.
3
5
  *
@@ -436,6 +438,12 @@ interface LoopEvent {
436
438
  content: string;
437
439
  reasoningDelta?: string;
438
440
  toolName?: string;
441
+ /**
442
+ * Raw JSON-string arguments the model sent for a tool call (role === "tool").
443
+ * Populated so transcripts can persist *why* a tool was called, not just
444
+ * what it returned. Needed by `reasonix diff` to explain divergences.
445
+ */
446
+ toolArgs?: string;
439
447
  stats?: TurnStats;
440
448
  planState?: TypedPlanState;
441
449
  repair?: RepairReport;
@@ -463,6 +471,12 @@ interface CacheFirstLoopOptions {
463
471
  * since the default selector scores samples by plan-state uncertainty.
464
472
  */
465
473
  branch?: number | BranchOptions;
474
+ /**
475
+ * Session name. When set, the loop pre-loads the session's prior messages
476
+ * into its log on construction, and appends every new log entry to
477
+ * `~/.reasonix/sessions/<name>.jsonl` so the next run can resume.
478
+ */
479
+ session?: string;
466
480
  }
467
481
  /**
468
482
  * Pillar 1 — Cache-First Loop.
@@ -494,9 +508,13 @@ declare class CacheFirstLoop {
494
508
  harvestOptions: HarvestOptions;
495
509
  branchEnabled: boolean;
496
510
  branchOptions: BranchOptions;
511
+ sessionName: string | null;
512
+ /** Number of messages that were pre-loaded from the session file. */
513
+ readonly resumedMessageCount: number;
497
514
  private _turn;
498
515
  private _streamPreference;
499
516
  constructor(opts: CacheFirstLoopOptions);
517
+ private appendAndPersist;
500
518
  /**
501
519
  * Reconfigure model/harvest/branch/stream mid-session. The loop's log,
502
520
  * scratch, and stats are preserved — only the per-turn behavior changes.
@@ -510,6 +528,38 @@ declare class CacheFirstLoop {
510
528
  private assistantMessage;
511
529
  }
512
530
 
531
+ /**
532
+ * Session persistence.
533
+ *
534
+ * Every turn's log entries (user / assistant / tool messages) are appended to
535
+ * a JSONL file under `~/.reasonix/sessions/<name>.jsonl`. Next time the user
536
+ * starts the CLI with the same session name, the loop pre-loads the file
537
+ * into its AppendOnlyLog so the new turn has full prior context.
538
+ *
539
+ * Design notes:
540
+ * - JSONL rather than JSON so concurrent writes don't corrupt.
541
+ * - 0600 permissions on Unix (chmod no-ops on Windows).
542
+ * - Name sanitization keeps paths safe: only [\w-] and CJK letters pass;
543
+ * anything else is replaced with underscore, max 64 chars.
544
+ * - The loop's stats/session aren't persisted — only the message log.
545
+ * Cost accounting resets each run (by design — old costs are sunk).
546
+ */
547
+
548
+ interface SessionInfo {
549
+ name: string;
550
+ path: string;
551
+ size: number;
552
+ messageCount: number;
553
+ mtime: Date;
554
+ }
555
+ declare function sessionsDir(): string;
556
+ declare function sessionPath(name: string): string;
557
+ declare function sanitizeName(name: string): string;
558
+ declare function loadSessionMessages(name: string): ChatMessage[];
559
+ declare function appendSessionMessage(name: string, message: ChatMessage): void;
560
+ declare function listSessions(): SessionInfo[];
561
+ declare function deleteSession(name: string): boolean;
562
+
513
563
  /**
514
564
  * Minimal `.env` loader; no dependency on dotenv.
515
565
  *
@@ -519,6 +569,207 @@ declare class CacheFirstLoop {
519
569
  */
520
570
  declare function loadDotenv(path?: string): void;
521
571
 
572
+ /**
573
+ * Transcript format — the canonical "audit log" of a Reasonix session.
574
+ *
575
+ * Design split:
576
+ * - Session file (`~/.reasonix/sessions/<name>.jsonl`) stores only the
577
+ * `ChatMessage`s the model needs to resume. See session.ts.
578
+ * - Transcript file (this module) stores every LoopEvent with usage, cost,
579
+ * model, and prefix fingerprint attached where available — enough for
580
+ * replay and diff to reconstruct economics.
581
+ *
582
+ * The two are different contracts: sessions are the user's *memory*;
583
+ * transcripts are the *receipts*. Don't conflate them.
584
+ *
585
+ * Backward compatibility: all fields beyond {ts, turn, role, content} are
586
+ * optional on read. A v0.1 transcript (pre-usage) still parses and renders
587
+ * — it just shows cost/cache as n/a.
588
+ */
589
+
590
+ interface TranscriptRecord {
591
+ /** ISO-8601 timestamp at emit time. */
592
+ ts: string;
593
+ /** 1-based turn number within the session. */
594
+ turn: number;
595
+ /** LoopEvent role — "assistant_delta" | "assistant_final" | "tool" | "done" | ... */
596
+ role: string;
597
+ /** For assistant events, the final (or delta) text; for tool events, the tool result. */
598
+ content: string;
599
+ /** Tool name (role === "tool"). */
600
+ tool?: string;
601
+ /** JSON-string args the model sent for a tool call (role === "tool"). Persisted so diff can explain *why* two runs made different calls. */
602
+ args?: string;
603
+ /** DeepSeek token-usage snapshot (role === "assistant_final"). */
604
+ usage?: RawUsage;
605
+ /** USD cost of this turn (role === "assistant_final"). */
606
+ cost?: number;
607
+ /** Model id that produced this turn. */
608
+ model?: string;
609
+ /**
610
+ * The ImmutablePrefix fingerprint at this turn. Lets diff prove two runs
611
+ * share a prefix — i.e. any cache-hit delta is attributable to log
612
+ * stability, not to a different system prompt.
613
+ */
614
+ prefixHash?: string;
615
+ /** Optional error message (role === "error"). */
616
+ error?: string;
617
+ }
618
+ interface TranscriptMeta {
619
+ /**
620
+ * Optional metadata written as the first line of a transcript. Lets
621
+ * downstream tooling know what it's reading without guessing.
622
+ * Recognized by a special role "_meta".
623
+ */
624
+ version: 1;
625
+ source: string;
626
+ model?: string;
627
+ task?: string;
628
+ mode?: string;
629
+ repeat?: number;
630
+ startedAt: string;
631
+ }
632
+ interface ReadTranscriptResult {
633
+ meta: TranscriptMeta | null;
634
+ records: TranscriptRecord[];
635
+ }
636
+ /**
637
+ * Build a TranscriptRecord from a LoopEvent. Extra fields (model,
638
+ * prefixHash) that the LoopEvent doesn't carry are passed in separately
639
+ * because they're session-level, not event-level.
640
+ */
641
+ declare function recordFromLoopEvent(ev: LoopEvent, extra: {
642
+ model: string;
643
+ prefixHash: string;
644
+ }): TranscriptRecord;
645
+ /**
646
+ * Append a record to an open write stream. Caller owns the stream lifecycle.
647
+ */
648
+ declare function writeRecord(stream: WriteStream, record: TranscriptRecord): void;
649
+ /**
650
+ * Write a _meta line to an open write stream. Call exactly once, at the top.
651
+ */
652
+ declare function writeMeta(stream: WriteStream, meta: TranscriptMeta): void;
653
+ /**
654
+ * Convenience: open a stream, write meta, return stream.
655
+ */
656
+ declare function openTranscriptFile(path: string, meta: TranscriptMeta): WriteStream;
657
+ /**
658
+ * Parse a transcript file. Returns meta (if the first line is a _meta record)
659
+ * and the full record list.
660
+ *
661
+ * Robustness contract:
662
+ * - Empty lines are skipped.
663
+ * - Malformed JSON lines are skipped silently (do not crash on partial
664
+ * files — live chats may be mid-write).
665
+ * - Records missing optional fields still parse — they're just rendered
666
+ * with n/a where the optional value would go.
667
+ */
668
+ declare function readTranscript(path: string): ReadTranscriptResult;
669
+ declare function parseTranscript(raw: string): ReadTranscriptResult;
670
+
671
+ /**
672
+ * Replay — reconstruct session economics from a transcript file.
673
+ *
674
+ * Given a transcript written by App.tsx or the bench runner, rebuild a
675
+ * SessionSummary-compatible aggregate (turn count, total cost, cache-hit
676
+ * ratio, vs-Claude estimate) without replaying the LLM calls.
677
+ *
678
+ * The whole point is offline auditing: a reader should be able to reproduce
679
+ * the headline numbers from a transcript alone, without an API key.
680
+ */
681
+
682
+ interface ReplayStats extends SessionSummary {
683
+ /** Per-turn stats, in turn order. Only assistant_final records contribute. */
684
+ perTurn: TurnStats[];
685
+ /** Unique models that appeared in the transcript's assistant_final records. */
686
+ models: string[];
687
+ /** Unique prefix hashes that appeared. Length > 1 means the prefix churned (cache-hostile). */
688
+ prefixHashes: string[];
689
+ /** Count of user-role records (user turns issued). */
690
+ userTurns: number;
691
+ /** Count of tool-role records (tool calls executed). */
692
+ toolCalls: number;
693
+ }
694
+ /**
695
+ * Parse a transcript file and compute replay stats. Throws only on I/O
696
+ * errors; malformed lines inside the file are skipped silently.
697
+ */
698
+ declare function replayFromFile(path: string): {
699
+ parsed: ReadTranscriptResult;
700
+ stats: ReplayStats;
701
+ };
702
+ declare function computeReplayStats(records: TranscriptRecord[]): ReplayStats;
703
+
704
+ /**
705
+ * Diff — compare two transcripts and produce a summary + divergence report.
706
+ *
707
+ * Two transcripts are "comparable" when they stem from the same task (or
708
+ * the same user prompt). Alignment is by turn number: assistant_final #N
709
+ * in A pairs with assistant_final #N in B. If one side ran more turns, the
710
+ * extras are labeled "only in A" / "only in B".
711
+ *
712
+ * What we compute:
713
+ * - Aggregate deltas: turns, tool calls, cache hit, cost, token counts
714
+ * - First divergence: the lowest turn where A and B's tool calls or
715
+ * assistant text differ meaningfully
716
+ * - Prefix-stability story: how many unique prefix hashes each side used
717
+ *
718
+ * Non-goals (deliberately):
719
+ * - LLM-judge quality comparison
720
+ * - Per-token delta rendering — not useful at the fidelity we're at
721
+ * - Embedding similarity — Levenshtein ratio is cheap and good enough
722
+ */
723
+
724
+ interface DiffSide {
725
+ label: string;
726
+ meta: ReadTranscriptResult["meta"];
727
+ records: TranscriptRecord[];
728
+ stats: ReplayStats;
729
+ }
730
+ interface TurnPair {
731
+ turn: number;
732
+ aAssistant?: TranscriptRecord;
733
+ bAssistant?: TranscriptRecord;
734
+ aTools: TranscriptRecord[];
735
+ bTools: TranscriptRecord[];
736
+ /**
737
+ * Classification of the pair:
738
+ * "match" — both sides present, text & tool calls within threshold
739
+ * "diverge" — both sides present, but text or tool calls differ
740
+ * "only_in_a" — assistant_final in A but not B
741
+ * "only_in_b" — assistant_final in B but not A
742
+ */
743
+ kind: "match" | "diverge" | "only_in_a" | "only_in_b";
744
+ /** When kind === "diverge", a short one-liner pointing at what differs. */
745
+ divergenceNote?: string;
746
+ }
747
+ interface DiffReport {
748
+ a: DiffSide;
749
+ b: DiffSide;
750
+ pairs: TurnPair[];
751
+ firstDivergenceTurn: number | null;
752
+ }
753
+ declare function diffTranscripts(a: {
754
+ label: string;
755
+ parsed: ReadTranscriptResult;
756
+ }, b: {
757
+ label: string;
758
+ parsed: ReadTranscriptResult;
759
+ }): DiffReport;
760
+ /**
761
+ * Normalized Levenshtein similarity ratio in [0, 1]. 1 = identical.
762
+ * Early-exits for long strings (> 2000 chars) with a cheap token-overlap
763
+ * estimate to keep diff fast on chatty transcripts.
764
+ */
765
+ declare function similarity(a: string, b: string): number;
766
+ interface RenderOptions {
767
+ /** Monochrome output (for file redirection or piping). Defaults to true. */
768
+ monochrome?: boolean;
769
+ }
770
+ declare function renderSummaryTable(report: DiffReport, _opts?: RenderOptions): string;
771
+ declare function renderMarkdown(report: DiffReport): string;
772
+
522
773
  /**
523
774
  * User-level config storage for the Reasonix CLI.
524
775
  *
@@ -546,6 +797,6 @@ declare function redactKey(key: string): string;
546
797
 
547
798
  /** Reasonix — DeepSeek-native agent framework. Library entry point. */
548
799
 
549
- declare const VERSION = "0.0.1";
800
+ declare const VERSION = "0.2.0";
550
801
 
551
- export { AppendOnlyLog, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, CacheFirstLoop, type CacheFirstLoopOptions, type ChatMessage, type ChatResponse, DeepSeekClient, type DeepSeekClientOptions, type EventRole, type FlattenDecision, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type JSONSchema, type LoopEvent, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, SessionStats, type SessionSummary, StormBreaker, type StreamChunk, type ToolCall, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TruncationRepairResult, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, aggregateBranchUsage, analyzeSchema, claudeEquivalentCost, costUsd, defaultConfigPath, defaultSelector, emptyPlanState, fetchWithRetry, flattenSchema, harvest, isPlanStateEmpty, isPlausibleKey, loadApiKey, loadDotenv, nestArguments, readConfig, redactKey, repairTruncatedJson, runBranches, saveApiKey, scavengeToolCalls, writeConfig };
802
+ export { AppendOnlyLog, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, CacheFirstLoop, type CacheFirstLoopOptions, type ChatMessage, type ChatResponse, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EventRole, type FlattenDecision, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type JSONSchema, type LoopEvent, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, type SessionInfo, SessionStats, type SessionSummary, StormBreaker, type StreamChunk, type ToolCall, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, aggregateBranchUsage, analyzeSchema, appendSessionMessage, claudeEquivalentCost, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenSchema, harvest, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, runBranches, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, writeConfig, writeMeta, writeRecord };