Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions use-cases/surenkotian/living-document-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SUPERDOCS_API_KEY=your-key-here
SUPERDOCS_BASE_URL=https://api.superdocs.app
6 changes: 6 additions & 0 deletions use-cases/surenkotian/living-document-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
__pycache__/
*.pyc
.venv/
agent_state.json
.pytest_cache/
116 changes: 116 additions & 0 deletions use-cases/surenkotian/living-document-agent/PROOF_RUN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Proof run — real terminal output, real API, real account

Account created via `POST /v1/agents/signup` (self-signup flow documented at docs.superdocs.app), free tier, 500 ops/month. Every call below is real; nothing in this file is a description of expected behavior.

## 1. Bootstrap (first run, no document exists yet)

```
$ python living_doc_agent.py --source source_data/status_v1.json
[living-doc-agent] no existing document in state -- creating the standing document for the first time
[living-doc-agent] create response: I have created the 'Team Operations Dashboard' document with the priorities, notes, and service status table as requested.
[living-doc-agent] created document b3faf2bc-f5da-4cb5-91f4-6609cf34d9d6, owned section = 2 chunk(s): ['e756e234-b5fe-4f98-b12f-5191015f2f89', '411f0464-77eb-4220-98b6-69e5f08de603']
[living-doc-agent] bootstrap complete. Re-run with a changed --source file to see an update cycle.
```

Resulting document (fetched via `GET /v1/documents/{id}?include_html=true` immediately after):

```html
<h1 data-chunk-id="ef20f84f-a3e9-4149-86cb-d52ed76cb45d">Team Operations Dashboard</h1>
<h2 data-chunk-id="84d98a95-db3d-45c9-ad20-5f0b92a301ee">Weekly Priorities</h2>
<ol data-chunk-id="b34ba260-9486-472e-9673-c2e4b53b5ca3"><li>Ship the Q3 customer portal redesign.</li><li>Close out vendor renewal negotiations before end of month.</li><li>Onboard two new SRE hires.</li></ol>
<h2 data-chunk-id="9267d12f-8918-4ee6-adba-3f5030b64bd6">Team Notes</h2>
<p data-chunk-id="da73c3db-6086-4285-90cc-a42d6721b7b7">Standup moved to 9:15am starting Monday. Ping Priya if you're out.</p>
<h2 data-chunk-id="e756e234-b5fe-4f98-b12f-5191015f2f89">[AGENT-MAINTAINED] Live Service Status -- do not edit manually</h2>
<table data-chunk-id="411f0464-77eb-4220-98b6-69e5f08de603"><thead><tr><th>Service</th><th>Status</th><th>Latency</th></tr></thead><tbody><tr><td>API Gateway</td><td>Operational</td><td>42ms</td></tr><tr><td>Auth Service</td><td>Operational</td><td>55ms</td></tr><tr><td>Billing Worker</td><td>Operational</td><td>120ms</td></tr><tr><td colspan="3">Last synced: 2026-08-01T09:00:00Z</td></tr></tbody></table>
```

## 2. Real update cycle (Auth Service degrades)

```
$ python living_doc_agent.py --source source_data/status_v2.json --auto-approve
[living-doc-agent] source changed: b13ccd3f7584... -> f1b8a999b9f8...
[living-doc-agent] submitted job 27dcfc01-052a-4459-8612-e90edf2b1a57, polling...
[living-doc-agent] approved 1 change(s), all confirmed scoped to the owned section
[living-doc-agent] boundary check passed: all 5 chunks outside the owned section are byte-identical before/after
[living-doc-agent] update complete
```

Document afterward -- only the table's own chunk_id (`411f0464-...`) changed; every other chunk_id and its content is byte-identical to step 1:

```html
<h1 data-chunk-id="ef20f84f-a3e9-4149-86cb-d52ed76cb45d">Team Operations Dashboard</h1>
<h2 data-chunk-id="84d98a95-db3d-45c9-ad20-5f0b92a301ee">Weekly Priorities</h2>
<ol data-chunk-id="b34ba260-9486-472e-9673-c2e4b53b5ca3">...same three items, unchanged...</ol>
<h2 data-chunk-id="9267d12f-8918-4ee6-adba-3f5030b64bd6">Team Notes</h2>
<p data-chunk-id="da73c3db-6086-4285-90cc-a42d6721b7b7">Standup moved to 9:15am starting Monday. Ping Priya if you're out.</p>
<h2 data-chunk-id="e756e234-b5fe-4f98-b12f-5191015f2f89">[AGENT-MAINTAINED] Live Service Status -- do not edit manually</h2>
<table data-chunk-id="411f0464-77eb-4220-98b6-69e5f08de603"><thead><tr><th>Service</th><th>Status</th><th>Latency</th></tr></thead><tbody><tr><td>API Gateway</td><td>Operational</td><td>45ms</td></tr><tr><td>Auth Service</td><td>Degraded</td><td>890ms</td></tr><tr><td>Billing Worker</td><td>Operational</td><td>118ms</td></tr><tr><td colspan="3">Last synced: 2026-08-01T15:00:00Z</td></tr></tbody></table>
```

## 3. Stopping rule / non-self-retriggering, proven with `whoami`, not asserted

```
$ curl -s https://api.superdocs.app/v1/agents/whoami -H "Authorization: Bearer $KEY" | grep used
"used":6

$ python living_doc_agent.py --source source_data/status_v2.json --auto-approve
[living-doc-agent] source unchanged (hash f1b8a999b9f8...) -- nothing to do, no API call made

$ curl -s https://api.superdocs.app/v1/agents/whoami -H "Authorization: Bearer $KEY" | grep used
"used":6
```

The agent's own edit in step 2 changed the document. Re-running against the *same source* immediately afterward did not treat that change as new work — because the stopping condition is keyed to the external source hash, never to the document's own state. `used` is bit-for-bit identical before and after.

## 4. Second real update (recovery)

```
$ python living_doc_agent.py --source source_data/status_v3_recovered.json --auto-approve
[living-doc-agent] source changed: f1b8a999b9f8... -> 10d49247d030...
[living-doc-agent] submitted job 7defca5e-968f-4601-a8c7-e4034d37a40e, polling...
[living-doc-agent] approved 1 change(s), all confirmed scoped to the owned section
[living-doc-agent] boundary check passed: all 5 chunks outside the owned section are byte-identical before/after
[living-doc-agent] update complete
```

## 5. No-spend preview mode, proven with `whoami`

```
$ curl -s .../whoami | grep used
"used":7

$ python living_doc_agent.py --source source_data/status_v2.json --preview
[living-doc-agent] source changed: 10d49247d030... -> f1b8a999b9f8...
[living-doc-agent] PREVIEW MODE (no-spend): the following would be sent, but no chat call is made:
----------------------------------------------------------------------
In the section headed exactly "[AGENT-MAINTAINED] Live Service Status -- do not edit manually", replace the existing table ENTIRELY with a new table matching the following description...
----------------------------------------------------------------------
[living-doc-agent] preview complete -- 0 operations spent (verify with whoami)

$ curl -s .../whoami | grep used
"used":7
```

## 6. `agent_state.json` run_history — a readable history, not churn

```json
[
{"timestamp": "2026-08-06T16:08:21Z", "action": "created", "document_id": "b3faf2bc-..."},
{"timestamp": "2026-08-06T16:09:00Z", "action": "updated", "source_hash": "f1b8a999...", "boundary_verified": true},
{"timestamp": "2026-08-06T16:09:22Z", "action": "noop", "source_hash": "f1b8a999..."},
{"timestamp": "2026-08-06T16:10:09Z", "action": "updated", "source_hash": "10d49247...", "boundary_verified": true}
]
```

Four entries for four runs against three distinct source states plus one repeat — not four full-document rewrites.

## The bug this proof run actually caught

The *first* attempt at step 2 (against a document created before the table/paragraph redesign described in README.md) produced this, verbatim, from the live API:

```html
<table data-chunk-id="57c00e34-...">...(correctly updated table)...</table><p>Last synced: 2026-08-01T15:00:00Z</p>
<p data-chunk-id="78c828e9-...">Last synced: 2026-08-01T09:00:00Z</p>
```

Two "Last synced" lines. The new one has **no `data-chunk-id` at all** — it was inserted as fresh content, not an edit to the existing paragraph, which the old one still was. The boundary-diff code itself also had a bug at this point (it only checked chunks present in both before/after snapshots, so an *added* chunk outside the section didn't register as a detected change in the log message, even though the raw equality check correctly still failed the run). Both are fixed in the shipped version — the table now carries its own timestamp as a spanning row, and the diff check now reports added/removed/changed separately. Re-run afterward (step 2 above) is the fixed, clean result.
75 changes: 75 additions & 0 deletions use-cases/surenkotian/living-document-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Living-document agent

Built by Suren Kotian for the SuperDocs Founding Engineer task.

An agent that owns one named section of a standing SuperDocs document -- "**[AGENT-MAINTAINED] Live Service Status**" -- and keeps it current from a changing external source, without ever touching the human-written sections around it. Everything below was run against the real SuperDocs API with a real (free-tier) account; nothing here is mocked.

## What it does

The standing document, "Team Operations Dashboard", has three sections:

- **Weekly Priorities** -- human-written, static
- **Team Notes** -- human-written, static
- **[AGENT-MAINTAINED] Live Service Status** -- owned by this agent, regenerated from `source_data/*.json` (standing in for a real monitoring feed)

Every run:

1. Hashes the current source file.
2. **Unchanged since the last run → stop. Zero API calls.** This is the stopping rule, and it's also what makes the agent structurally incapable of re-triggering on its own output: it only ever reacts to the *external source* changing, never to the document changing, so its own edit can never look like new work to itself.
3. Changed, with `--preview` → fetch the current document (a free read) and print what *would* change. No `chat` call, no approval, no cost.
4. Changed, live → propose the update via `chat_async(approval_mode="ask_every_time")`, refuse to approve anything that touches a chunk outside the owned section, and after approval **independently re-fetch the document and hash-compare every chunk outside the section against a snapshot taken before the edit**. If anything else moved, that's a hard failure -- logged loudly, not swallowed.

## Surfaces used

- **MCP-equivalent (REST)** -- the task doc states anything specified against the REST API may be built on MCP and vice versa; this build uses the REST surface (`/v1/chat`, `/v1/chat/async`, `/v1/chat/{session}/approve`, `/v1/documents/{id}`), which exposes the identical four core operations MCP does.
- **Review** -- every ongoing update goes through `chat_async` + `approve_change`, SuperDocs' own human-in-the-loop mechanism, not a direct sync edit.
- **Search** -- the owned section is re-located every run by searching the document's own structure for its heading text (`locate_owned_section` in `living_doc_agent.py`), rather than trusting a remembered chunk_id blindly. A manually reorganized document is still found correctly.
- **Memory** -- not used in the shipped version; see "What I'd add next" below for why, honestly, rather than bolted on for the sake of touching the surface.

## Proving the ownership boundary is never crossed

`chunking.py` parses the document's real HTML into its top-level `data-chunk-id` blocks (verified against real API output, not assumed from docs). Before every edit, every chunk **outside** the owned section is hashed. After the edit, they're hashed again. If they don't match byte-for-byte, the run fails loudly. This isn't a claim -- it's `assert before_snapshot == after_snapshot` against real data fetched from the live API, both times.

## Real proof run (not a description)

Full transcript in `PROOF_RUN.md`. Summary, with real numbers from the account's own `whoami`:

| Step | Source | ops before -> after | Result |
|---|---|---|---|
| Bootstrap | `status_v1.json` | 2 -> 3 | Document created, owned section = 2 chunks (heading + table) |
| Update | `status_v2.json` (Auth Service degrades) | 3 -> 6 | Table updated in place; **5 other chunks byte-identical before/after** |
| Re-run, same source | `status_v2.json` | 6 -> 6 | **Zero API calls** -- stopping rule, proven via `whoami`, not asserted |
| Update | `status_v3_recovered.json` (recovery) | 6 -> 7 | Table updated again; boundary check passed again |
| Preview | `status_v2.json` (after already applying v3) | 7 -> 7 | Diff printed, **zero cost**, confirmed via `whoami` |

`agent_state.json`'s `run_history` (also in `PROOF_RUN.md`) reads `created -> updated -> noop -> updated` -- a readable history, not four full-document rewrites.

A PDF export of the final live document is at `team_operations_dashboard_final.pdf` (exports don't cost operations).

## A real bug, found and fixed, not papered over

The first live update revealed a genuine design flaw. The original instruction said "replace the table **and the Last synced line**" -- two separate things, one a table cell, one a standalone paragraph. The model didn't edit the old paragraph in place; it **inserted a new paragraph** with the fresh timestamp and left the stale one sitting there, orphaned, without even a `data-chunk-id`. Two "Last synced" lines, one of them dead. Exactly the "churn instead of a readable history" failure this build is supposed to prevent.

Fix: folded the timestamp **into the table** as a spanning final row, so there is exactly one dynamic element to replace, not two things to keep in sync. "Replace this whole table with an updated table" turned out to be unambiguous in a way "also update that line somewhere below" wasn't. Re-ran clean afterward -- see the second row of the table above. Full detail in `PROOF_RUN.md`.

Separately, the post-hoc boundary-diff comparison itself had a bug: it only checked chunks that existed in *both* the before and after snapshot, so a chunk that got **added** outside the owned section (exactly what the paragraph-duplication bug produced) wouldn't have been caught by the diff logic as a "changed" chunk. Fixed to check added/removed/changed separately. Both bugs are logged here because they were both real, found by actually running this against the live API, not by reasoning about the code.

## What I'd add next, honestly

- **Cross-session memory** wasn't used in the end. I considered writing agent-preference notes into SuperDocs' `cross_session_memory` so the agent could recall its own maintenance conventions across sessions with no local state at all. Decided against it for this submission: the local `agent_state.json` (chunk IDs, last source hash, run history) is the thing that actually has to be correct for the boundary guarantee, and duplicating that into a second, server-side, less-inspectable store would have added a real state-reconciliation problem (which one wins if they disagree?) without making the core guarantee any stronger. Worth doing for a version that needs to survive losing `agent_state.json` entirely.
- The pre-approval boundary check (reject a proposed change before it's even approved, if it targets an out-of-scope chunk) is implemented and exercised by the code path, but never actually triggered in this proof run because the model's proposed changes stayed correctly scoped every time -- so it's real code, live-key-testable, but not yet caught a live violation. The post-hoc check (which did catch the paragraph-duplication issue) is the one with a real save.

## Running it

```bash
python -m venv .venv && source .venv/Scripts/activate # or .venv/bin/activate on Mac/Linux
pip install -r requirements.txt
cp .env.example .env # fill in SUPERDOCS_API_KEY (agent self-signup: POST /v1/agents/signup)

python living_doc_agent.py --source source_data/status_v1.json # bootstrap
python living_doc_agent.py --source source_data/status_v2.json --preview # no-spend preview
python living_doc_agent.py --source source_data/status_v2.json --auto-approve # live update
python living_doc_agent.py --source source_data/status_v2.json --auto-approve # no-op (same source)
```

Omit `--auto-approve` to review each proposed change interactively before it lands.
Loading