@terreno/mcp 57.8.0 → 57.10.1
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/docs/upgrades/57.10.1.md +54 -0
- package/dist/docs/upgrades/57.9.0.md +28 -0
- package/dist/docs/versioned/next/README.md +1 -0
- package/dist/docs/versioned/next/explanation/README.md +4 -0
- package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
- package/dist/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
- package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
- package/dist/docs/versioned/next/explanation/ai-observability.md +101 -0
- package/dist/docs/versioned/next/explanation/charts.md +12 -0
- package/dist/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
- package/dist/docs/versioned/next/explanation/example-coverage.md +4 -2
- package/dist/docs/versioned/next/explanation/loop-engineering.md +1 -1
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +49 -1
- package/dist/docs/versioned/next/explanation/typescript-configuration.md +1 -1
- package/dist/docs/versioned/next/explanation/versioning-policy.md +1 -1
- package/dist/docs/versioned/next/how-to/README.md +7 -1
- package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +1 -1
- package/dist/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
- package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
- package/dist/docs/versioned/next/how-to/ai-feature-development.md +156 -0
- package/dist/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
- package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +7 -0
- package/dist/docs/versioned/next/how-to/circleci.md +9 -6
- package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +8 -0
- package/dist/docs/versioned/next/how-to/install-agent-skills.md +1 -1
- package/dist/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -1
- package/dist/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
- package/dist/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
- package/dist/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
- package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
- package/dist/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
- package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +3 -2
- package/dist/docs/versioned/next/implementationPlans/dashboard-chart-parity.md +275 -0
- package/dist/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
- package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
- package/dist/docs/versioned/next/reference/README.md +3 -0
- package/dist/docs/versioned/next/reference/admin-config.md +2 -0
- package/dist/docs/versioned/next/reference/admin-frontend.md +216 -0
- package/dist/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
- package/dist/docs/versioned/next/reference/ai-harness.md +1881 -0
- package/dist/docs/versioned/next/reference/ai.md +283 -28
- package/dist/docs/versioned/next/reference/api.md +46 -0
- package/dist/docs/versioned/next/reference/blocks.md +178 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +2 -1
- package/dist/docs/versioned/next/reference/feature-flags.md +1 -1
- package/dist/docs/versioned/next/reference/mcp-server.md +17 -0
- package/dist/docs/versioned/next/reference/test.md +11 -2
- package/dist/docs/versioned/next/reference/ui.md +418 -11
- package/dist/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
- package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
- package/dist/docs/versioned/next/tasks/ai-observability.md +233 -104
- package/dist/docs/versioned/next/tasks/dashboard-chart-parity.md +145 -0
- package/dist/docs/versioned/next/tasks/durable-agent-harness.md +183 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +37 -0
- package/dist/tools.js.map +1 -1
- package/package.json +3 -2
- package/src/docs/upgrades/57.10.1.md +54 -0
- package/src/docs/upgrades/57.9.0.md +28 -0
- package/src/docs/versioned/next/README.md +1 -0
- package/src/docs/versioned/next/explanation/README.md +4 -0
- package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
- package/src/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
- package/src/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
- package/src/docs/versioned/next/explanation/ai-observability.md +101 -0
- package/src/docs/versioned/next/explanation/charts.md +12 -0
- package/src/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
- package/src/docs/versioned/next/explanation/example-coverage.md +4 -2
- package/src/docs/versioned/next/explanation/loop-engineering.md +1 -1
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +49 -1
- package/src/docs/versioned/next/explanation/typescript-configuration.md +1 -1
- package/src/docs/versioned/next/explanation/versioning-policy.md +1 -1
- package/src/docs/versioned/next/how-to/README.md +7 -1
- package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +1 -1
- package/src/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
- package/src/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
- package/src/docs/versioned/next/how-to/ai-feature-development.md +156 -0
- package/src/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
- package/src/docs/versioned/next/how-to/charts-and-dashboards.md +7 -0
- package/src/docs/versioned/next/how-to/circleci.md +9 -6
- package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +8 -0
- package/src/docs/versioned/next/how-to/install-agent-skills.md +1 -1
- package/src/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -1
- package/src/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
- package/src/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
- package/src/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
- package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
- package/src/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
- package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +3 -2
- package/src/docs/versioned/next/implementationPlans/dashboard-chart-parity.md +275 -0
- package/src/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
- package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
- package/src/docs/versioned/next/reference/README.md +3 -0
- package/src/docs/versioned/next/reference/admin-config.md +2 -0
- package/src/docs/versioned/next/reference/admin-frontend.md +216 -0
- package/src/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
- package/src/docs/versioned/next/reference/ai-harness.md +1881 -0
- package/src/docs/versioned/next/reference/ai.md +283 -28
- package/src/docs/versioned/next/reference/api.md +46 -0
- package/src/docs/versioned/next/reference/blocks.md +178 -0
- package/src/docs/versioned/next/reference/environment-variables.md +2 -1
- package/src/docs/versioned/next/reference/feature-flags.md +1 -1
- package/src/docs/versioned/next/reference/mcp-server.md +17 -0
- package/src/docs/versioned/next/reference/test.md +11 -2
- package/src/docs/versioned/next/reference/ui.md +418 -11
- package/src/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
- package/src/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
- package/src/docs/versioned/next/tasks/ai-observability.md +233 -104
- package/src/docs/versioned/next/tasks/dashboard-chart-parity.md +145 -0
- package/src/docs/versioned/next/tasks/durable-agent-harness.md +183 -0
|
@@ -36,7 +36,7 @@ org switcher, dark mode).
|
|
|
36
36
|
| C | Communications (pluggable) | `comms-abstraction`, one item **per adapter**: `comms-adapter-expo-push` (first push adapter, per D4), `comms-adapter-twilio-sms`, `comms-adapter-twilio-verify`, `comms-adapter-sendgrid` (first mail adapter, per D2); operations: `comms-admin-dashboard`; consumers: `password-reset-and-email-verification`, `notification-center` |
|
|
37
37
|
| B | Billing | `inbound-webhooks`, `billing-stripe` (web-first, per D1), `mobile-iap-revenuecat` (Future) |
|
|
38
38
|
| D | Zero-to-deployed DX | `create-terreno-app`, `mongo-migrations` (deploy items already exist in the OSS launch program) |
|
|
39
|
-
| E | B2B UI surfaces | `data-grid-server-filters`, `charts-and-dashboards`, `dark-mode-theme`, `command-palette`, `wizard-stepper`, `wysiwyg-editor` (markdown stays for now, per D3), `global-search` |
|
|
39
|
+
| E | B2B UI surfaces | `data-grid-server-filters`, `charts-and-dashboards`, `dashboard-chart-parity`, `dark-mode-theme`, `command-palette`, `wizard-stepper`, `wysiwyg-editor` (markdown stays for now, per D3), `global-search` |
|
|
40
40
|
| F | Enterprise & scale | `framework-audit-log` (Next), `rate-limiting` (Next), `job-queues` (Next), `enterprise-sso`, `mfa-step-up-auth` |
|
|
41
41
|
| — | Native baseline | `native-module-baseline` — all new native modules land in one major release |
|
|
42
42
|
|
|
@@ -194,8 +194,9 @@ None.
|
|
|
194
194
|
|
|
195
195
|
## Not Included / Future Work
|
|
196
196
|
|
|
197
|
-
-
|
|
198
|
-
-
|
|
197
|
+
- Ops-dashboard parity (scorecards, comparison sparklines, additive multi-series, donut centers, spanning grid, table chrome) — [`dashboard-chart-parity.md`](dashboard-chart-parity.md).
|
|
198
|
+
- Composition grammar (Phase 6) — recorded Grow decisions; implement on a follow-up Pick. Not required for the How’s it going dashboard.
|
|
199
|
+
- Multi-series cartesian via additive `series` lands in `dashboard-chart-parity`; grammar `dataKey` / `stackId` remains later.
|
|
199
200
|
- Wiring `CommsDashboardScreen` charts to the stats endpoint.
|
|
200
201
|
- Example-frontend dashboard.
|
|
201
202
|
- Zoom/pan.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Implementation Plan: Dashboard chart parity
|
|
2
|
+
|
|
3
|
+
**Status:** Approved — 2026-09-25
|
|
4
|
+
**Branch:** `cursor/dashboard-chart-parity-8a50`
|
|
5
|
+
**Owner:** —
|
|
6
|
+
**Created:** 2026-09-24
|
|
7
|
+
**Roadmap issue:** — (create via `roadmap-item` after this IP is Approved; follow-up to [#1175](https://github.com/TerrenoLabs/terreno/issues/1175))
|
|
8
|
+
**Task list:** [dashboard-chart-parity.md](../tasks/dashboard-chart-parity.md)
|
|
9
|
+
**Depends on:** [charts-and-dashboards.md](charts-and-dashboards.md) (shipped Phases 1–5)
|
|
10
|
+
**RTK deprecation flag:** None
|
|
11
|
+
**Program:** [B2B platform](b2b-platform-program.md) track E
|
|
12
|
+
|
|
13
|
+
## Goal
|
|
14
|
+
|
|
15
|
+
Make `@terreno/ui` able to compose an ops dashboard like the “How’s it going?”
|
|
16
|
+
reference: KPI scorecards with comparison sparklines, titled chart cards with period
|
|
17
|
+
badges, multi-series lines, donut hole labels, spanning grids, and Looker-like table
|
|
18
|
+
chrome — without copying Looker paint, and without requiring the Chart children grammar.
|
|
19
|
+
|
|
20
|
+
App authors rebuild that page from first-class primitives (`Scorecard`, `SparklineChart`,
|
|
21
|
+
`ChartCard`, spanning `DashboardGrid`, additive chart facades, `DataTable` extras). Proof
|
|
22
|
+
is a demo gallery fixture of the reference layout plus rendered PNG goldens.
|
|
23
|
+
|
|
24
|
+
## Non-Goals
|
|
25
|
+
|
|
26
|
+
- Pixel-identical Looker colors, fonts, or screenshot matching of the PDF JPEG.
|
|
27
|
+
- Shipping a date-range picker. Period badges may call `onPeriodPress`.
|
|
28
|
+
- Variable `DataTable` row height (would break virtualization).
|
|
29
|
+
- Dual-period numbers inside one table cell (current + previous stacked in the same cell).
|
|
30
|
+
- Replacing `{label, value}[]` facades with composition-only `Chart*` children.
|
|
31
|
+
- Implementing the Phase 6 composition grammar in this slice (still future work on
|
|
32
|
+
`charts-and-dashboards`).
|
|
33
|
+
- Example-frontend screen, comms dashboard wiring, zoom/pan, combo charts, maps, heatmaps.
|
|
34
|
+
- Cross-chart filtering / drill-down.
|
|
35
|
+
|
|
36
|
+
## Decisions
|
|
37
|
+
|
|
38
|
+
| Question | Decision |
|
|
39
|
+
|----------|----------|
|
|
40
|
+
| Scope | All chart surfaces, headers, period badges, KPI cards, spanning grid, and the table chrome named below. Not the whole PDF pixel-for-pixel. |
|
|
41
|
+
| Easy-build surface | `ChartCard` (title + period badge + child) + `Scorecard` + spanning `DashboardGrid`. |
|
|
42
|
+
| Scorecards | Dedicated `Scorecard` facade plus reusable `SparklineChart`. |
|
|
43
|
+
| Comparison | First-class `comparisonData` with dotted stroke on sparkline and optional cartesian overlay. |
|
|
44
|
+
| Facades vs grammar | Keep `LineChart` / `BarChart` / `AreaChart` / `DonutChart`. Add additive props. This dashboard must not require a composition tree. |
|
|
45
|
+
| Visual language | Capabilities are theme-configurable. Default paint stays Terreno tokens, not a Looker preset. |
|
|
46
|
+
| Responsive grid | Configurable `span` on items. Reference fixture: KPI row 5→2→1; lower chart row 3→1. |
|
|
47
|
+
| Tables | Same IP. After the chart tracer: period header groups, sticky totals row, `1–50 of N` range status. Reuse existing pin/sort/pagination. |
|
|
48
|
+
| Wrapped names | Wrap up to two lines inside the existing `rowHeight`. Keep virtualization. |
|
|
49
|
+
| Period badge | Label plus optional `onPress`. No picker in this slice. |
|
|
50
|
+
|
|
51
|
+
## Architecture
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
DashboardGrid
|
|
55
|
+
DashboardGridItem span={{sm,md,lg}}
|
|
56
|
+
ChartCard title periodLabel onPeriodPress?
|
|
57
|
+
Scorecard | LineChart | BarChart | DonutChart | DataTable
|
|
58
|
+
Scorecard
|
|
59
|
+
SparklineChart (plot only; solid + dotted comparison)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Existing facades stay the simple path. `ChartCard` is chrome around any child (chart or
|
|
63
|
+
table). Facade `title` / `periodLabel` / `onPeriodPress` are shortcuts that render the
|
|
64
|
+
same header inside the chart; callers use **either** `ChartCard` **or** those props, not
|
|
65
|
+
both.
|
|
66
|
+
|
|
67
|
+
### Public props (additive)
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface ChartSeries {
|
|
71
|
+
color?: string;
|
|
72
|
+
data: Array<{color?: string; label: string; value: number}>;
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
stroke?: "solid" | "dotted";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface CartesianChartProps {
|
|
79
|
+
comparisonData?: Array<{label: string; value: number}>;
|
|
80
|
+
data: Array<{color?: string; label: string; value: number}>;
|
|
81
|
+
formatValue?: (value: number) => string;
|
|
82
|
+
onPeriodPress?: () => void;
|
|
83
|
+
periodLabel?: string;
|
|
84
|
+
series?: ChartSeries[];
|
|
85
|
+
title?: string;
|
|
86
|
+
xTickPolicy?: "auto" | "truncate" | "rotate";
|
|
87
|
+
// existing: height, legendLabel, emptyText, loading, accessibilityLabel, testID
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface DonutChartProps extends CartesianChartProps {
|
|
91
|
+
centerTitle?: string;
|
|
92
|
+
centerValue?: string;
|
|
93
|
+
formatShare?: (value: number, total: number) => string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface SparklineChartProps {
|
|
97
|
+
comparisonData?: Array<{label: string; value: number}>;
|
|
98
|
+
data: Array<{label: string; value: number}>;
|
|
99
|
+
height?: number;
|
|
100
|
+
testID?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ScorecardProps {
|
|
104
|
+
comparisonData?: Array<{label: string; value: number}>;
|
|
105
|
+
formatValue?: (value: number) => string;
|
|
106
|
+
onPeriodPress?: () => void;
|
|
107
|
+
periodLabel?: string;
|
|
108
|
+
sparklineData?: Array<{label: string; value: number}>;
|
|
109
|
+
title: string;
|
|
110
|
+
value: number | string;
|
|
111
|
+
testID?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface ChartCardProps {
|
|
115
|
+
children: React.ReactNode;
|
|
116
|
+
filterSummary?: string;
|
|
117
|
+
onPeriodPress?: () => void;
|
|
118
|
+
periodLabel?: string;
|
|
119
|
+
title: string;
|
|
120
|
+
testID?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface DashboardGridItemProps {
|
|
124
|
+
children: React.ReactNode;
|
|
125
|
+
span?: {lg?: number; md?: number; sm?: number};
|
|
126
|
+
testID?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface DataTableProps {
|
|
130
|
+
footerRows?: DataTableCellData[][];
|
|
131
|
+
headerGroups?: Array<{columnCount: number; subtitle?: string; title: string}>;
|
|
132
|
+
rangeStart?: number;
|
|
133
|
+
rangeTotal?: number;
|
|
134
|
+
wrapLines?: 1 | 2; // default 1; 2 = wrap inside rowHeight
|
|
135
|
+
// existing page / setPage / totalPages remain; range copy uses page size math
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`series` when present is the cartesian data source (`data` is ignored except as a
|
|
140
|
+
single-series fallback). `comparisonData` aligns on `label` and paints a dotted stroke
|
|
141
|
+
(sparkline/line) or a lighter overlay (bar — if labels match). Default `xTickPolicy` is
|
|
142
|
+
`"auto"`: rotate ~45° when labels would truncate; keep horizontal for ≤7 categorical
|
|
143
|
+
labels.
|
|
144
|
+
|
|
145
|
+
Donut legend rows become `swatch + label + formatShare` (default percent of total).
|
|
146
|
+
`centerTitle` / `centerValue` sit in the hole via overlay `Text`.
|
|
147
|
+
|
|
148
|
+
`DashboardGridItem` span is in column units of the current breakpoint count (span 5 in a
|
|
149
|
+
5-column KPI row is full width on `sm` when `columns.sm === 1`).
|
|
150
|
+
|
|
151
|
+
`SparklineChart` draws no axes, grid, legend, or reserved tooltip row. Hit targets stay
|
|
152
|
+
≥24px when press/hover is wired; default is non-interactive (KPI sparkline).
|
|
153
|
+
|
|
154
|
+
`headerGroups` paint a second header row above `columns` (period title + optional
|
|
155
|
+
subtitle such as `(Previous period)`). `footerRows` stick to the bottom of the scroll
|
|
156
|
+
body. Range status is `rangeStart–rangeEnd of rangeTotal` next to existing pagination.
|
|
157
|
+
|
|
158
|
+
### File layout
|
|
159
|
+
|
|
160
|
+
| Path | Role |
|
|
161
|
+
|------|------|
|
|
162
|
+
| `ui/src/SparklineChart.tsx` | Public sparkline |
|
|
163
|
+
| `ui/src/Scorecard.tsx` | Public KPI tile |
|
|
164
|
+
| `ui/src/ChartCard.tsx` | Public title + period badge chrome |
|
|
165
|
+
| `ui/src/DashboardGridItem.tsx` | Public span wrapper (or named export from `DashboardGrid.tsx` — no barrel) |
|
|
166
|
+
| `ui/src/LineChart.tsx` etc. | Additive `series`, `comparisonData`, ticks, header shortcuts |
|
|
167
|
+
| `ui/src/DonutChart.tsx` | Center stack + share legend |
|
|
168
|
+
| `ui/src/DataTable.tsx` | Header groups, footer rows, range status, wrapLines |
|
|
169
|
+
| `ui/src/Pagination.tsx` | Range copy when totals provided |
|
|
170
|
+
| `demo/chartVisual/fixtureCatalog.ts` | New hard fixtures including `hows-it-going-dashboard` |
|
|
171
|
+
| `docs/how-to/charts-and-dashboards.md` | Rewrite around Scorecard + ChartCard + spans |
|
|
172
|
+
|
|
173
|
+
`DashboardGrid` stays eager. New chart widgets join `heavyOptionalExports`. `ChartCard`
|
|
174
|
+
and `Scorecard` are layout+text heavy enough to sit with charts (lazy). `DashboardGridItem`
|
|
175
|
+
stays eager with the grid.
|
|
176
|
+
|
|
177
|
+
### Chart visual regression
|
|
178
|
+
|
|
179
|
+
Add fixtures that exercise the reference patterns (not the JPEG pixels):
|
|
180
|
+
|
|
181
|
+
| id | Pattern |
|
|
182
|
+
|----|---------|
|
|
183
|
+
| `scorecard-sparkline-comparison` | Five KPI tiles, solid + dotted sparkline |
|
|
184
|
+
| `bar-time-rotated-ticks` | Vertical bars, rotated dates, y `formatValue` |
|
|
185
|
+
| `line-three-series` | Three lines, markers, per-series legend |
|
|
186
|
+
| `bar-day-of-week` | Seven categorical bars, horizontal ticks |
|
|
187
|
+
| `donut-center-and-share` | Hole label + percent legend |
|
|
188
|
+
| `donut-single-slice` | One 100% slice + center total |
|
|
189
|
+
| `hows-it-going-dashboard` | Spanning grid: 5 KPIs, time bar, 3-line, DOW bar, two donuts, one table card |
|
|
190
|
+
|
|
191
|
+
`bun run ui:charts:compare` must cover these. Agents use `review-chart-visuals`.
|
|
192
|
+
|
|
193
|
+
## Models / APIs / Notifications
|
|
194
|
+
|
|
195
|
+
None.
|
|
196
|
+
|
|
197
|
+
## UI
|
|
198
|
+
|
|
199
|
+
Universal React Native + web. Demo stories + visual gallery only. Tables stay `DataTable`
|
|
200
|
+
inside `ChartCard`.
|
|
201
|
+
|
|
202
|
+
## Phases
|
|
203
|
+
|
|
204
|
+
1. **Tracer:** `SparklineChart` + `Scorecard` + `ChartCard` + `comparisonData` on the
|
|
205
|
+
sparkline; demo story; docs stub; golden for `scorecard-sparkline-comparison`.
|
|
206
|
+
2. **Cartesian additive:** `series`, `comparisonData`, `xTickPolicy`, header shortcuts on
|
|
207
|
+
line/bar/area; goldens for time bar and 3-line.
|
|
208
|
+
3. **Donut:** center stack + share legend; goldens for two-slice and 100% slice.
|
|
209
|
+
4. **Grid spans:** `DashboardGridItem`; reference KPI wrap; compose `hows-it-going-dashboard`
|
|
210
|
+
charts (table slot placeholder ok until Phase 5).
|
|
211
|
+
5. **Table chrome:** header groups, footer rows, range status, `wrapLines={2}`; table card
|
|
212
|
+
in the hard fixture.
|
|
213
|
+
6. **Packaging + docs:** lazy exports, import regression, how-to rewrite, explanation,
|
|
214
|
+
reference, TypeDoc, changelog, ui rule export list.
|
|
215
|
+
|
|
216
|
+
## Feature Flags & Migrations
|
|
217
|
+
|
|
218
|
+
None. Additive public props. Existing `{label, value}[]` callers stay valid. Not a major.
|
|
219
|
+
|
|
220
|
+
`legendLabel` remains for single-series cartesian. Multi-series uses `series[].label`.
|
|
221
|
+
Donut still ignores `legendLabel`.
|
|
222
|
+
|
|
223
|
+
## Activity Log & User Updates
|
|
224
|
+
|
|
225
|
+
None.
|
|
226
|
+
|
|
227
|
+
## Not Included / Future Work
|
|
228
|
+
|
|
229
|
+
- Chart children grammar (`Chart`, `ChartXAxis`, …) — still `charts-and-dashboards` Phase 6.
|
|
230
|
+
- Date-range picker control.
|
|
231
|
+
- Dual-period table cells.
|
|
232
|
+
- Variable row height.
|
|
233
|
+
- Example-frontend / comms wiring.
|
|
234
|
+
- Zoom/pan, combo charts.
|
|
235
|
+
|
|
236
|
+
## Files to Create / Modify
|
|
237
|
+
|
|
238
|
+
See the task list. Docs in-slice:
|
|
239
|
+
|
|
240
|
+
- `docs/how-to/charts-and-dashboards.md` (rewrite)
|
|
241
|
+
- `docs/explanation/charts.md` (multi-series, scorecard, why not grammar-first)
|
|
242
|
+
- `docs/reference/ui.md` (new exports)
|
|
243
|
+
- `docs/how-to/compare-chart-rendered-snapshots.md` (new fixture ids)
|
|
244
|
+
- `.rulesync/rules/ui/00-ui.mdc`
|
|
245
|
+
- `changelog/unreleased/dashboard-chart-parity.md`
|
|
246
|
+
- TypeDoc props (`bun run website:generate` in the docs task)
|
|
247
|
+
|
|
248
|
+
## Task List
|
|
249
|
+
|
|
250
|
+
[docs/tasks/dashboard-chart-parity.md](../tasks/dashboard-chart-parity.md)
|
|
251
|
+
|
|
252
|
+
## Acceptance Criteria
|
|
253
|
+
|
|
254
|
+
- [ ] `Scorecard`, `SparklineChart`, and `ChartCard` are importable from `@terreno/ui` and subpaths.
|
|
255
|
+
- [ ] A scorecard shows title, formatted value, and a sparkline with dotted `comparisonData`.
|
|
256
|
+
- [ ] `LineChart` renders three series from `series` with a legend item each.
|
|
257
|
+
- [ ] Time-series x ticks can rotate; day-of-week ticks stay horizontal under `auto`.
|
|
258
|
+
- [ ] `DonutChart` draws `centerTitle` / `centerValue` and percent legend rows.
|
|
259
|
+
- [ ] `DashboardGridItem` spans reproduce 5→2→1 KPI and 3→1 chart wrapping in the reference fixture.
|
|
260
|
+
- [ ] `DataTable` shows period header groups, a sticky totals row, `1–50 of N`, and two-line wrap inside `rowHeight`.
|
|
261
|
+
- [ ] Existing single-series `{label, value}[]` charts and tests stay green.
|
|
262
|
+
- [ ] `bun run ui:charts:compare` matches goldens including `hows-it-going-dashboard`.
|
|
263
|
+
- [ ] How-to shows the easy-build composition (Scorecard + ChartCard + spans), not only raw `Card` + `LineChart`.
|
|
264
|
+
- [ ] Chart implementations stay lazy on the root; grid item stays eager.
|
|
265
|
+
- [ ] Frontend verification: demo gallery exercised on web; artifacts on the implementation PR.
|
|
266
|
+
|
|
267
|
+
## Risks
|
|
268
|
+
|
|
269
|
+
| Risk | Mitigation |
|
|
270
|
+
|------|------------|
|
|
271
|
+
| Dual header APIs (`ChartCard` vs facade title) | Document exclusive use; tests assert no double header when only one is set |
|
|
272
|
+
| `DataTable` wrap vs virtualization | Cap at two lines; keep `rowHeight`; screenshot a wrapped first column |
|
|
273
|
+
| Additive `series` vs later grammar | Facades remain the supported easy path; grammar stays a follow-up |
|
|
274
|
+
| Golden flake on dense dashboards | One hard fixture at a fixed gallery width; review-chart-visuals on mismatch |
|
|
275
|
+
| Header groups vs pinned columns | Totals and groups must align with `pinnedColumns` scroll sync; regression tests |
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Implementation Plan: Durable agent harness (`@terreno/ai/harness`)
|
|
2
|
+
|
|
3
|
+
**Status:** Approved
|
|
4
|
+
**Roadmap:** Area=`ai`, Target=`Next`, Impact=`Feature`
|
|
5
|
+
**Branch:** `durable-agent-harness` (stacked on `cursor/ai-observability-ip-64ca`)
|
|
6
|
+
**Owner:** unassigned
|
|
7
|
+
**Created:** 2026-10-02
|
|
8
|
+
**Task list:** [docs/tasks/durable-agent-harness.md](../tasks/durable-agent-harness.md)
|
|
9
|
+
**Inspiration:** [Pi Durable](https://earendil.com/posts/pi-durable/) (concepts only; no dependency)
|
|
10
|
+
**Depends on:** [ai-observability.md](./ai-observability.md) (`ObsTrace` / `ObsSpan`, `ObservabilityApp`), [job-queues.md](./job-queues.md) (`@terreno/jobs`, optional)
|
|
11
|
+
**Supersedes in part:** [ai-agents-and-failover.md](./ai-agents-and-failover.md) — its `Agent` and failover ideas become harness primitives; its "no human-in-the-loop / no resume / no sub-agents" non-goals are reversed here.
|
|
12
|
+
|
|
13
|
+
## Goal
|
|
14
|
+
|
|
15
|
+
One easy-to-use harness inside `@terreno/ai` that runs long-lived agentic work durably:
|
|
16
|
+
deterministic multi-phase workflows, LLM agent loops, tools, subagents, human approvals,
|
|
17
|
+
retries, and crash recovery — all audited. It must be strong enough to build an
|
|
18
|
+
OpenClaw/Hermes-class personal agent, a coding agent, and a regulated clinical automation
|
|
19
|
+
on the same primitives.
|
|
20
|
+
|
|
21
|
+
Today a chat turn lives inside one HTTP request (`ai/src/routes/gpt.ts`), history is saved
|
|
22
|
+
only after the stream ends, and a crash loses the turn. `@terreno/jobs` is durable but a
|
|
23
|
+
job is one opaque handler with no checkpoints. The harness fills that gap.
|
|
24
|
+
|
|
25
|
+
## Non-Goals
|
|
26
|
+
|
|
27
|
+
- Depending on or wrapping `@earendil-works/pi-durable`.
|
|
28
|
+
- Journaled/replayed `step.run` workflows (Temporal/Inngest style). Phases only.
|
|
29
|
+
- Socket.io streaming for agent output. Streaming is SSE only.
|
|
30
|
+
- Auto-migrating in-flight runs across task versions.
|
|
31
|
+
- Pluggable non-Mongo storage (SQLite/JSONL/memory). Mongo for every runner.
|
|
32
|
+
- Field-level encryption, retention/redaction policies, e-signatures (future work).
|
|
33
|
+
- Changing `/gpt/prompt` behavior before Phase 4.
|
|
34
|
+
|
|
35
|
+
## Decisions
|
|
36
|
+
|
|
37
|
+
| Question | Decision |
|
|
38
|
+
|----------|----------|
|
|
39
|
+
| Build our own harness or adopt pi-durable? | Own harness on Mongo + Vercel `ai` SDK, copying Pi's concepts (tasks, checkpoints, replay-safe tools, hooks, memos, documents, compaction, forks). |
|
|
40
|
+
| Where should the harness live? | Inside `@terreno/ai`, as subpath `@terreno/ai/harness`. |
|
|
41
|
+
| How should the `@terreno/jobs` dependency be wired? | Optional peer. Jobs integration is optional; a single process can own execution. |
|
|
42
|
+
| Who executes a run? | Pluggable `Runner`: `InProcessRunner` (single owner, default) and `JobsRunner` (leased, multi-instance). |
|
|
43
|
+
| How do durable code steps resume after a crash? | Explicit phases (Pi-style `defineTask` state machine); each phase commits a checkpoint. |
|
|
44
|
+
| Where does state live for the single-process owner? | Mongo for both runners; switching runners needs no migration. |
|
|
45
|
+
| What compliance guarantees ship in v1? | Immutable step/event audit log, built on the AI observability branch's `ObsTrace` / `ObsSpan`. |
|
|
46
|
+
| How does the audit log relate to `ObsTrace` / `ObsSpan`? | `ObsSpan` **is** the audit log for harness work. |
|
|
47
|
+
| How do we guarantee a span exists for every checkpoint? | Checkpoint commit and its `ObsSpan` write happen in one Mongo transaction. Langfuse/OTel stay best-effort after commit. |
|
|
48
|
+
| How does this sequence against the observability branch? | Branch off `cursor/ai-observability-ip-64ca` and rebase as it lands. |
|
|
49
|
+
| What happens on resume when replay was not declared? | Never auto-replay. Interrupted tools are reported to the model; interrupted phases go to `interrupted`. Opt in with `replay: "safe"`. |
|
|
50
|
+
| Which features land in Phase 1 with the clinical tracer? | Tasks, phases, agent loop, tools + replay, retries, audit, **hooks + memos + approvals**, **subagents + ownership tree**. |
|
|
51
|
+
| How do clients watch runs live? | SSE only, resumable via `Last-Event-ID`; a separate submit endpoint sends input/steer. |
|
|
52
|
+
| How does a clinician sign off on an approval? | An approval inbox in `@terreno/admin-frontend`. |
|
|
53
|
+
| Who can see and act on an approval? | Per-approval `approvers` (modelRouter permission shape); inbox lists only what the user may approve; default `IsAdmin`; mountable outside admin chrome. |
|
|
54
|
+
| What happens to `/gpt/prompt`? | Untouched in Phase 1; rebuilt on the harness in Phase 4. |
|
|
55
|
+
| Where do coding-agent execution environments land? | Phase 1 defines the `ExecutionEnv` interface only; Phase 5 ships Node env, coding tools, sandbox adapter. |
|
|
56
|
+
| Task definition changes while runs are in flight? | Runs pin `name@version`; old versions must stay registered; startup fails loudly otherwise. |
|
|
57
|
+
|
|
58
|
+
### Recorded assumptions (low-risk, chosen by convention)
|
|
59
|
+
|
|
60
|
+
- **Interrupted model requests are re-requested.** A model call has no side effect, so a
|
|
61
|
+
crash mid-stream marks the partial response aborted and the turn retries the request.
|
|
62
|
+
- **Model-call retries:** 429/5xx/network errors retry with exponential backoff + jitter
|
|
63
|
+
(default 3 attempts), then an optional `fallbackModels` list. A thrown phase retries
|
|
64
|
+
under the task's `retry` policy (default `maxAttempts: 3`); exhausted → `failed`.
|
|
65
|
+
- **Token deltas are persisted, coalesced** (~250 ms) as `HarnessEvent` rows with a TTL so
|
|
66
|
+
any instance can serve an SSE resume. Committed messages are permanent.
|
|
67
|
+
- **Cross-instance SSE** tails `HarnessEvent` with a Mongo change stream (replica set is
|
|
68
|
+
already required).
|
|
69
|
+
- **Non-local trace sinks** (Langfuse, OTel) receive the full trace once a root task is
|
|
70
|
+
terminal, best-effort, via `ObservabilityApp.exportTrace`.
|
|
71
|
+
- **Approvals inbox filtering** evaluates `approvers` per pending approval in memory
|
|
72
|
+
(pending sets are small); pagination applies after filtering.
|
|
73
|
+
|
|
74
|
+
## Architecture
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
app code: defineTask / defineAgent / defineTool / defineExtension
|
|
78
|
+
|
|
|
79
|
+
Harness (registry + storage)
|
|
80
|
+
+-------------+-------------+--------------+----------------+
|
|
81
|
+
| | | | |
|
|
82
|
+
Task engine Agent loop Hooks/memos Approvals Event log
|
|
83
|
+
(phases, (built-in (extensions) (wait + inbox) (SSE source)
|
|
84
|
+
checkpoints, task)
|
|
85
|
+
ownership)
|
|
86
|
+
|
|
|
87
|
+
runtime.commit() ── one Mongo transaction ──> HarnessTask/Message/Memo/Event
|
|
88
|
+
+ ObsSpan / ObsTrace (audit)
|
|
89
|
+
|
|
|
90
|
+
Runner: InProcessRunner (owner lease) | JobsRunner (@terreno/jobs, optional)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Core concepts
|
|
94
|
+
|
|
95
|
+
| Concept | What it is |
|
|
96
|
+
|---------|-----------|
|
|
97
|
+
| **Task** | Durable unit of work: `name@version`, `input`, `state`, `phase`, status, ownership. Everything (model request, tool call, workflow, subagent) is a task. |
|
|
98
|
+
| **Phase** | A named async function of a task. It does work and calls `rt.commit()` with the next phase or a terminal outcome. A phase is the replay boundary. |
|
|
99
|
+
| **Checkpoint** | The committed `{phase, state}`. Resume always starts the current phase from its checkpoint. |
|
|
100
|
+
| **Conversation** | A transcript plus agent config (model, instructions, tools, extensions). Root, or owned by a task (subagent). |
|
|
101
|
+
| **Agent** | `defineAgent` config. Running an agent = the built-in `terreno.agent.turn` task on a conversation. |
|
|
102
|
+
| **Tool** | `defineTool` with zod params and `replay: "safe" | "never"` (default `never`). Runs as a child task of the turn. |
|
|
103
|
+
| **Extension** | Named bundle of prompt sections, tools, hooks, and tool wraps. |
|
|
104
|
+
| **Hook** | `beforeModelRequest`, `beforeTool`, `afterTool`. Can rewrite, block, or annotate. |
|
|
105
|
+
| **Memo** | `rt.memo(key, value?)` — durable decision storage scoped to a task, survives restarts. |
|
|
106
|
+
| **Approval** | `rt.approval(key, {...})` — creates a `HarnessApproval`, waits for decision, audits the decider. |
|
|
107
|
+
| **Event** | Append-only `HarnessEvent` rows with per-conversation/task `seq`; the SSE source. |
|
|
108
|
+
| **Runner** | Decides who executes runnable tasks and when. |
|
|
109
|
+
|
|
110
|
+
### Commit and audit
|
|
111
|
+
|
|
112
|
+
`rt.commit(fn)` opens a Mongo transaction that, fenced by the task's lease token:
|
|
113
|
+
|
|
114
|
+
1. Updates `HarnessTask` (`phase`, `state`, status, `attempt`, outcome).
|
|
115
|
+
2. Inserts any `HarnessMessage`, `HarnessMemo`, `HarnessApproval`, `HarnessEvent` rows.
|
|
116
|
+
3. Inserts the `ObsSpan` for the finished work and updates the root `ObsTrace`.
|
|
117
|
+
|
|
118
|
+
If the lease token no longer matches (another owner took over) the transaction aborts and
|
|
119
|
+
the phase result is discarded. `Harness` construction throws when the local observability
|
|
120
|
+
plugin (`createLocalObservabilityPlugin`) is not registered or the connection is not a
|
|
121
|
+
replica set.
|
|
122
|
+
|
|
123
|
+
Span mapping: root task → `ObsTrace` + `CHAIN` root span; phase → `CHAIN`; model request →
|
|
124
|
+
`LLM` (usage, cost via `priceMap`); tool → `TOOL`; subagent → `AGENT`; approval decision →
|
|
125
|
+
`CHAIN` named `approval:<key>` with `decidedBy`, `decision`, `reason` in `output`. Spans
|
|
126
|
+
are never updated after insert except `endedAt`/`status` on the same commit.
|
|
127
|
+
|
|
128
|
+
### Replay and interruption
|
|
129
|
+
|
|
130
|
+
On resume, a task whose status is `running` with an expired lease was interrupted:
|
|
131
|
+
|
|
132
|
+
| Interrupted thing | `replay: "safe"` | default (`never`) |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| Phase | Re-run the phase from its checkpoint | Status `interrupted`; await `resolveInterrupted` (`retry` / `abort` / `complete`) |
|
|
135
|
+
| Tool call | Re-run the tool | Tool result = "interrupted, not retried"; agent loop continues |
|
|
136
|
+
| Model request | Always re-requested (assumption above) | — |
|
|
137
|
+
|
|
138
|
+
### Runners
|
|
139
|
+
|
|
140
|
+
| Runner | Ownership | Use |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `InProcessRunner` (default) | One process holds the `HarnessOwner` lease (heartbeat). A second process gets `standby` and takes over on lease expiry. Polls runnable tasks; wakes on `runAt` and events. | Local agents, single-instance apps, tests |
|
|
143
|
+
| `JobsRunner` (`@terreno/ai/harness/jobsRunner`, optional peer `@terreno/jobs`) | Each runnable phase → job `terreno.harness.phase`, idempotency key `taskId:phase:attempt`. Handler acquires the task lease (fenced, heartbeat) and runs one phase. The task lease, not the job lock, is the authority. | Multi-instance Cloud Run |
|
|
144
|
+
|
|
145
|
+
### Ownership tree
|
|
146
|
+
|
|
147
|
+
Tasks are owned by a conversation or a task. `abort(taskId)` aborts owned tasks
|
|
148
|
+
bottom-up, running each task's `abort` handler (compensations) before marking it
|
|
149
|
+
`aborted`. `background: true` tasks belong to a conversation but are not part of its
|
|
150
|
+
current turn; a turn abort leaves them running.
|
|
151
|
+
|
|
152
|
+
### Versioning
|
|
153
|
+
|
|
154
|
+
Each task records `name` + `version`. The runner only resumes a task on a registered
|
|
155
|
+
handler for exactly that version. `Harness.start()` scans non-terminal tasks and throws
|
|
156
|
+
listing every unregistered `name@version`.
|
|
157
|
+
|
|
158
|
+
## Models
|
|
159
|
+
|
|
160
|
+
All in `ai/src/harness/models/`, types in `ai/src/types/harness.ts`. Every field has
|
|
161
|
+
`description`; `strict: "throw"`.
|
|
162
|
+
|
|
163
|
+
| Model | Key fields |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `HarnessTask` | `name`, `version`, `input`, `state`, `phase`, `status` (`pending` \| `running` \| `waiting` \| `interrupted` \| `completed` \| `failed` \| `aborted`), `ownership {kind: conversation\|task\|root, id}`, `background`, `attempt`, `retry {maxAttempts, backoffMs, maxBackoffMs}`, `runAt`, `lease {owner, token, expiresAt}`, `waiting {kind: event\|tasks\|sleep, key, taskIds, policy, timeoutAt}`, `outcome {status, result, error}`, `traceId`, `rootTaskId`, `requestId` (unique sparse), `userId` |
|
|
166
|
+
| `HarnessConversation` | `ownership`, `agent {name, model, instructions, tools[], extensions[]}`, `status` (`idle` \| `busy`), `activeTurnTaskId`, `queued[]`, `userId`, `seq` |
|
|
167
|
+
| `HarnessMessage` | `conversationId`, `seq`, `role`, `parts[]`, `toolCallId`, `toolName`, `turnTaskId`, `aborted` |
|
|
168
|
+
| `HarnessMemo` | `taskId`, `key`, `value` — unique `(taskId, key)` |
|
|
169
|
+
| `HarnessApproval` | `taskId`, `rootTaskId`, `definitionKey` (`name@version:key`), `title`, `summary`, `payload`, `status` (`pending` \| `approved` \| `rejected` \| `expired`), `decidedBy`, `decidedAt`, `reason`, `expiresAt` |
|
|
170
|
+
| `HarnessEvent` | `streamId` (conversation or task), `seq`, `type`, `payload`, TTL for `delta` events |
|
|
171
|
+
| `HarnessOwner` | singleton lease for `InProcessRunner`: `owner`, `expiresAt` |
|
|
172
|
+
|
|
173
|
+
## APIs
|
|
174
|
+
|
|
175
|
+
### Library (`@terreno/ai/harness`)
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import {
|
|
179
|
+
Harness, defineTask, defineAgent, defineTool, defineExtension, hook, section,
|
|
180
|
+
approvalGate, InProcessRunner,
|
|
181
|
+
} from "@terreno/ai/harness";
|
|
182
|
+
|
|
183
|
+
const summarizer = defineAgent({
|
|
184
|
+
name: "clinic.summarizer",
|
|
185
|
+
model: {provider: "anthropic", modelId: "claude-sonnet-5-5"},
|
|
186
|
+
instructions: "Summarize the chart for a clinician. Cite sources.",
|
|
187
|
+
tools: [],
|
|
188
|
+
fallbackModels: [{provider: "anthropic", modelId: "claude-haiku-4-5-20251001"}],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const intakeSummary = defineTask<{patientId: string}, IntakeState, {status: string}>({
|
|
192
|
+
name: "clinic.intakeSummary",
|
|
193
|
+
version: 1,
|
|
194
|
+
initial: () => ({phase: "fetch"}),
|
|
195
|
+
retry: {maxAttempts: 3},
|
|
196
|
+
phases: {
|
|
197
|
+
fetch: {
|
|
198
|
+
replay: "safe",
|
|
199
|
+
run: async (task, rt) => {
|
|
200
|
+
const chart = await ehr.getChart(task.input.patientId);
|
|
201
|
+
await rt.commit({phase: "summarize", state: {chart}});
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
summarize: {
|
|
205
|
+
replay: "safe",
|
|
206
|
+
run: async (task, rt) => {
|
|
207
|
+
const summary = await rt.runAgent(summarizer, {input: task.state.chart, output: SummarySchema});
|
|
208
|
+
await rt.commit({phase: "review", state: {...task.state, summary}});
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
review: {
|
|
212
|
+
replay: "safe",
|
|
213
|
+
run: async (task, rt) => {
|
|
214
|
+
// Approvers are declared on the definition: approvals: {"clinician-signoff": {approvers: [Permissions.IsAdmin, isClinician]}}
|
|
215
|
+
const decision = await rt.approval("clinician-signoff", {
|
|
216
|
+
title: "Sign off intake summary",
|
|
217
|
+
payload: task.state.summary,
|
|
218
|
+
timeout: {hours: 24},
|
|
219
|
+
});
|
|
220
|
+
await rt.commit(decision.approved ? {phase: "write"} : {terminal: {status: "completed", result: {status: "rejected"}}});
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
write: {
|
|
224
|
+
// default replay: "never" — an interrupted EHR write goes to `interrupted`
|
|
225
|
+
run: async (task, rt) => {
|
|
226
|
+
await ehr.writeNote(task.input.patientId, task.state.summary, {idempotencyKey: `note-${task.id}`});
|
|
227
|
+
await rt.commit({terminal: {status: "completed", result: {status: "filed"}}});
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
abort: async (task, rt) => { /* compensations */ },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const harness = await Harness.open({
|
|
235
|
+
models: createModelFn,
|
|
236
|
+
registry: [intakeSummary, summarizer, ClinicExtension],
|
|
237
|
+
runner: new InProcessRunner(),
|
|
238
|
+
});
|
|
239
|
+
await harness.start();
|
|
240
|
+
const run = await harness.createTask(intakeSummary, {patientId}, {requestId: `intake-${patientId}`});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Runtime (`rt`) surface in Phase 1: `commit`, `memo`, `runAgent`, `createTask`,
|
|
244
|
+
`waitForTasks(ids, {policy: "all" | "failFast"})`, `waitFor(event, {timeout})`,
|
|
245
|
+
`sleep(duration)`, `approval`, `output` (stream text to the event log), `signal`, `env`
|
|
246
|
+
(`ExecutionEnv`, interface only in Phase 1).
|
|
247
|
+
|
|
248
|
+
Harness surface: `open`, `start`, `stop`, `createTask`, `conversation(id)`,
|
|
249
|
+
`createConversation`, `sendEvent(taskId, event, payload)`, `abort(taskId)`,
|
|
250
|
+
`resolveInterrupted(taskId, action)`.
|
|
251
|
+
|
|
252
|
+
### HTTP (`HarnessApp` plugin)
|
|
253
|
+
|
|
254
|
+
| Method | Path | Behavior |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| POST | `/harness/conversations/:id/submit` | instanceAction. `{content, requestId, whenBusy: "queue" \| "steer"}`; idempotent on `requestId` |
|
|
257
|
+
| GET | `/harness/conversations/:id/events` | SSE; honors `Last-Event-ID`; replays then tails |
|
|
258
|
+
| GET | `/harness/tasks/:id/events` | SSE for a task subtree |
|
|
259
|
+
| POST | `/harness/tasks/:id/abort` | instanceAction; owner or admin |
|
|
260
|
+
| POST | `/harness/tasks/:id/resolveInterrupted` | instanceAction; admin; `{action: "retry" \| "abort" \| "complete", reason}` |
|
|
261
|
+
| GET | `/harness/approvals` | list pending approvals the caller may approve |
|
|
262
|
+
| POST | `/harness/approvals/:id/approve` \| `/reject` | instanceActions; `{reason}` (required on reject); audited span |
|
|
263
|
+
|
|
264
|
+
SSE is a listed exception to the modelRouter-actions rule; everything else is
|
|
265
|
+
`modelRouter` collection/instance actions.
|
|
266
|
+
|
|
267
|
+
## Notifications
|
|
268
|
+
|
|
269
|
+
None in Phase 1. `rt.approval` accepts an optional `notify` callback so apps can send via
|
|
270
|
+
`@terreno/comms`; the harness does not depend on comms.
|
|
271
|
+
|
|
272
|
+
## UI
|
|
273
|
+
|
|
274
|
+
- **Approvals inbox** (`@terreno/admin-frontend`, `widgets/harness/`): list of pending
|
|
275
|
+
approvals (title, task, age, expires), detail with `payload` rendered as JSON/markdown,
|
|
276
|
+
Approve / Reject with reason. Registered as admin custom screen `harness-approvals`
|
|
277
|
+
(group "AI Harness"), and exported as `HarnessApprovalInbox` for mounting outside admin
|
|
278
|
+
chrome. Uses `@terreno/ui` only.
|
|
279
|
+
- **Runs screen** (Phase 2): task list/tree, status filters, abort, resolve interrupted.
|
|
280
|
+
- Spans render in the existing observability Traces screens.
|
|
281
|
+
|
|
282
|
+
## Phases
|
|
283
|
+
|
|
284
|
+
| Phase | Ships | Proof |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| **1 — Durable clinical tracer** | Engine, phases, checkpoints, `InProcessRunner`, transactional `ObsSpan` audit, replay semantics, retries, agent loop, tools, subagents + ownership tree, extensions/hooks/memos, waits/events, approvals + inbox, SSE + submit, version pinning, example-backend `clinic.intakeSummary` | Crash-and-resume e2e; inbox Playwright screenshots; span tree assertions |
|
|
287
|
+
| **2 — Multi-instance** | `JobsRunner`, two-worker fencing, Runs admin screen | Two-process race tests; screenshots |
|
|
288
|
+
| **3 — Long context** | Background compaction, `reset`/handoff, conversation forks, `defineDoc` documents (rewindable, fork `asOf`) | Bun tests on 10k-message conversations; fork isolation tests |
|
|
289
|
+
| **4 — Durable chat + multiplayer** | `/gpt` rebuilt on harness, `GptHistory` mapping, steering, foreground/background abort, `viewState` over SSE, rtk `useHarnessConversation`, `GPTChat` adapter | Kill backend mid-chat, reconnect resumes; two clients see same view |
|
|
290
|
+
| **5 — Coding agents + malleability** | `NodeExecutionEnv`, `CodingTools` (read/write/edit/bash), remote sandbox adapter, registry hot-replace | Coding agent fixes a failing test in a fixture repo after a crash |
|
|
291
|
+
|
|
292
|
+
## Feature Flags & Migrations
|
|
293
|
+
|
|
294
|
+
- No feature flag; the harness is opt-in by registering `HarnessApp`.
|
|
295
|
+
- New collections only. Phase 4 adds a migration mapping `GptHistory` → `HarnessConversation`/`HarnessMessage` (human gate at Phase 4 start: keep dual-read or one-shot migrate).
|
|
296
|
+
|
|
297
|
+
## Activity Log & User Updates
|
|
298
|
+
|
|
299
|
+
Audit lives in `ObsSpan`. Changelog entry per phase in `changelog/`.
|
|
300
|
+
|
|
301
|
+
## Not Included / Future Work
|
|
302
|
+
|
|
303
|
+
Field encryption, retention/redaction, e-signature approvals, Socket.io streaming,
|
|
304
|
+
non-Mongo storage, journaled steps, in-flight version migration.
|
|
305
|
+
|
|
306
|
+
## Files to Create / Modify
|
|
307
|
+
|
|
308
|
+
| Path | Change |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `ai/src/harness/` | new: `harness.ts`, `defineTask.ts`, `runtime.ts`, `commit.ts`, `agentLoop.ts`, `defineAgent.ts`, `defineTool.ts`, `extensions.ts`, `approvals.ts`, `events.ts`, `modelCall.ts`, `runners/inProcessRunner.ts`, `runners/jobsRunner.ts`, `harnessApp.ts`, `routes/*.ts`, `models/*.ts` |
|
|
311
|
+
| `ai/src/types/harness.ts` | new model + API types |
|
|
312
|
+
| `ai/package.json` | `./harness`, `./harness/jobsRunner` exports; optional peer `@terreno/jobs` |
|
|
313
|
+
| `admin-frontend/src/widgets/harness/` | new approvals inbox (Phase 1), runs screen (Phase 2) |
|
|
314
|
+
| `example-backend/src/harness/` | `clinic.intakeSummary`, fake EHR, registration in `server.ts` |
|
|
315
|
+
| `example-frontend` | admin route for inbox; `bun run sdk` |
|
|
316
|
+
| `docs/explanation/durable-agent-harness.md` | new |
|
|
317
|
+
| `docs/reference/ai-harness.md` | new |
|
|
318
|
+
| `docs/how-to/build-a-durable-workflow.md` | new |
|
|
319
|
+
| `docs/reference/ai.md`, `docs/reference/admin-frontend.md` | link + inbox section |
|
|
320
|
+
|
|
321
|
+
## Task List
|
|
322
|
+
|
|
323
|
+
[docs/tasks/durable-agent-harness.md](../tasks/durable-agent-harness.md)
|
|
324
|
+
|
|
325
|
+
## Acceptance Criteria
|
|
326
|
+
|
|
327
|
+
| # | Criterion | Verification |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| AC1 | A two-phase task runs to completion and every checkpoint has exactly one `ObsSpan`, written in the same transaction | bun test: abort the transaction mid-commit → neither task update nor span exists |
|
|
330
|
+
| AC2 | Killing the owner mid-run resumes from the last checkpoint on a fresh `Harness` | bun test: throw-on-signal simulation + new harness instance; e2e: kill example-backend during `summarize`, restart, run completes |
|
|
331
|
+
| AC3 | Default-replay phases and tools never re-run after interruption; `replay: "safe"` ones do | bun test matrix over phase/tool × replay |
|
|
332
|
+
| AC4 | Thrown phases retry with backoff and land `failed` after `maxAttempts`; model 429/5xx retries then falls back | bun test with fake model provider |
|
|
333
|
+
| AC5 | Aborting a root task aborts owned subtasks bottom-up and runs `abort` handlers | bun test ordering assertion |
|
|
334
|
+
| AC6 | `beforeTool` hook can block a tool; memo decision survives restart | bun test |
|
|
335
|
+
| AC7 | `rt.approval` blocks until a permitted user approves; unpermitted users get 403 and do not see it in the inbox; decision span records `decidedBy` | supertest |
|
|
336
|
+
| AC8 | Approvals inbox lists, approves, and rejects in admin-frontend | Playwright run with screenshots in `/opt/cursor/artifacts/` |
|
|
337
|
+
| AC9 | SSE reconnect with `Last-Event-ID` replays missed events exactly once, from any instance | bun test with two `HarnessApp` instances on one replica set |
|
|
338
|
+
| AC10 | Starting with an in-flight unregistered `name@version` throws listing it | bun test |
|
|
339
|
+
| AC11 | `JobsRunner`: two workers never run the same phase concurrently; stale lease owner's commit is rejected | bun test (Phase 2) |
|
|
340
|
+
| AC12 | Docs explain, reference, and how-to every public API in the slice that ships it | `docs-audit` skill run shows no undocumented harness exports; Roast reads each page against the shipped API |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Roadmap issue:** https://github.com/TerrenoLabs/terreno/issues/1093
|
|
4
4
|
**Status:** Complete
|
|
5
5
|
|
|
6
|
-
> **See also:** Native in-app observability (prompts, traces, evals, experiments, review queue) is specified in [ai-observability.md](./ai-observability.md). This IP remains the **Langfuse Cloud/self-host vendor plugin**. Both plugins may be registered; traces/scores fan out (see
|
|
6
|
+
> **See also:** Native in-app observability (prompts, traces, evals, experiments, review queue) is specified in [ai-observability.md](./ai-observability.md). Operator loop: [Develop an AI feature](../how-to/ai-feature-development.md). This IP remains the **Langfuse Cloud/self-host vendor plugin**. Both plugins may be registered; traces/scores fan out (see the observability IP).
|
|
7
7
|
|
|
8
8
|
A TerrenoApp plugin that integrates [Langfuse](https://langfuse.com) into Terreno backends and frontends. Provides prompt management with MongoDB caching, OpenTelemetry tracing, Vercel AI SDK helpers, React hooks, and an admin UI — all wired up via `.install()`.
|
|
9
9
|
|
|
@@ -8,6 +8,9 @@ Technical reference for Terreno packages and APIs. Information-oriented, precise
|
|
|
8
8
|
- [@terreno/ui](ui.md) — Components, theming, layout
|
|
9
9
|
- [@terreno/syncdb](syncdb.md) — Local-first data layer (reads, writes, offline sync)
|
|
10
10
|
- [@terreno/ai](ai.md) — AI service, GPT routes, Langfuse integration
|
|
11
|
+
- [@terreno/ai/harness](ai-harness.md) — Durable multi-phase tasks with transactional checkpoints and audit spans
|
|
12
|
+
- [Agent UI Asks](agent-ui-asks.md) — `@terreno/blocks` ask schemas, simple cards, the compact surface, limits, error codes, SSE events, the headless `pendingAsks` and `turn` endpoints, and JSON Schemas for native clients
|
|
13
|
+
- [UI blocks](blocks.md) — whole-reply YAML for leaf and layout blocks: grammar, limits, and error codes
|
|
11
14
|
- [@terreno/admin-backend](admin-backend.md) — Auto-generated admin CRUD endpoints
|
|
12
15
|
- [@terreno/admin-frontend](admin-frontend.md) — Admin panel UI components
|
|
13
16
|
- [@terreno/admin-spa](admin-spa.md) — Standalone admin SPA + Express serve plugin
|