summaryrefslogtreecommitdiffhomepage
path: root/src/features/heartbeat/logic/types.ts
blob: 3d3d525e969caba095f2625ab62c695db543c450 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import type { ReasoningEffort } from "@dispatch/transport-contract";

/**
 * Pure core types for the heartbeat feature — zero DOM, zero effects, zero Svelte.
 *
 * Heartbeat is a workspace-scoped autonomous agent loop: the backend periodically
 * runs a turn in a dedicated conversation using a configured system prompt, task
 * prompt, model, reasoning effort, and interval. The FE exposes the config
 * (`GET`/`PUT /workspaces/:id/heartbeat`), the run history
 * (`GET /workspaces/:id/heartbeat/runs`), and a per-run stop
 * (`POST /workspaces/:id/heartbeat/runs/:runId/stop`).
 *
 * The backend's heartbeat API is a plain REST surface — it is NOT part of the
 * shared `@dispatch/transport-contract` / `@dispatch/wire` packages (verified:
 * no `heartbeat` symbol in either `dist/`). So, following the consumer-defines-
 * port pattern (mirrors `features/mcp` / `features/computer` result types), the
 * FE owns these shapes here and adapts the untyped JSON at the network seam in
 * the composition root. If the backend later promotes these to a shared contract
 * package, swap the local types for the imports (see `backend-handoff.md`).
 */

/** The canonical run lifecycle status (backend-owned enum, verbatim). */
export type HeartbeatRunStatus = "running" | "completed" | "stopped";

/** The workspace's heartbeat configuration (`GET /workspaces/:id/heartbeat`). */
export interface HeartbeatConfig {
  /** Whether the autonomous loop is enabled (running on the interval). */
  readonly enabled: boolean;
  readonly systemPrompt: string;
  readonly taskPrompt: string;
  /** Minutes between runs. */
  readonly intervalMinutes: number;
  /** The model name (`<credential>/<model>`) the heartbeat runs with. */
  readonly model: string;
  /**
   * The heartbeat's reasoning effort, or null when never set (the server
   * default `"high"` then applies) — mirrors the per-conversation knob's
   * resolution chain.
   */
  readonly reasoningEffort: ReasoningEffort | null;
}

/**
 * A partial config patch for `PUT /workspaces/:id/heartbeat`. Every field is
 * optional — the backend merges the patch onto the stored config.
 */
export interface HeartbeatConfigPatch {
  readonly enabled?: boolean;
  readonly systemPrompt?: string;
  readonly taskPrompt?: string;
  readonly intervalMinutes?: number;
  readonly model?: string;
  readonly reasoningEffort?: ReasoningEffort | null;
}

/** One heartbeat run (`GET /workspaces/:id/heartbeat/runs`). */
export interface HeartbeatRun {
  readonly id: string;
  /** The conversation this run wrote to (watch it live for the chat). */
  readonly conversationId: string;
  /** ISO timestamp of when the run was triggered. */
  readonly triggeredAt: string;
  readonly status: HeartbeatRunStatus;
}

// ── Injected ports (consumer-defines-port; the composition root adapts the
//    store's HTTP calls to these shapes). ──────────────────────────────────────

/** Outcome of `GET /workspaces/:id/heartbeat` (or the PUT response). */
export type HeartbeatConfigResult =
  | { readonly ok: true; readonly config: HeartbeatConfig }
  | { readonly ok: false; readonly error: string };

/** Outcome of `GET /workspaces/:id/heartbeat/runs`. */
export type HeartbeatRunsResult =
  | { readonly ok: true; readonly runs: readonly HeartbeatRun[] }
  | { readonly ok: false; readonly error: string };

/** Outcome of `POST /workspaces/:id/heartbeat/runs/:runId/stop`. */
export type HeartbeatStopResult =
  | { readonly ok: true }
  | { readonly ok: false; readonly error: string };

export type LoadHeartbeatConfig = () => Promise<HeartbeatConfigResult | null>;
export type SaveHeartbeatConfig = (
  patch: HeartbeatConfigPatch,
) => Promise<HeartbeatConfigResult | null>;
export type LoadHeartbeatRuns = () => Promise<HeartbeatRunsResult | null>;
export type StopHeartbeatRun = (runId: string) => Promise<HeartbeatStopResult | null>;

/**
 * Outcome of `GET /workspaces/:id/heartbeat/next-run` — the server-authoritative
 * timestamp of the next scheduled heartbeat run (ISO 8601 string), or `null`
 * when the heartbeat is disabled or no run is scheduled. The FE computes a live
 * countdown from this + a 1s clock (see `formatCountdown`). When the endpoint is
 * unavailable (404 — backend hasn't shipped it yet), the FE falls back to an
 * approximation from the runs + config (see `approximateNextRunEpoch`).
 */
export type HeartbeatNextRunResult =
  | { readonly ok: true; readonly nextRunAt: string | null }
  | { readonly ok: false; readonly error: string };

export type LoadHeartbeatNextRun = () => Promise<HeartbeatNextRunResult | null>;