Skip to content

Reproduce "Proactive Thought Partners" (arXiv:2609.01588) as a lab page - #634

Open
kcarnold wants to merge 6 commits into
mainfrom
claude/reproduce-arxiv-2609-thoughtful-r9fgwn
Open

Reproduce "Proactive Thought Partners" (arXiv:2609.01588) as a lab page#634
kcarnold wants to merge 6 commits into
mainfrom
claude/reproduce-arxiv-2609-thoughtful-r9fgwn

Conversation

@kcarnold

@kcarnold kcarnold commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

An attempt at reproducing Zhang, Davis, Chen & Hsu, Designing Proactive Thought Partners for Writing (arXiv:2609.01588v1) inside this add-in, plus an honest log of where the reproduction breaks down.

What the paper built

A Next.js app with its own BlockNote editor. Writers configure "partners" (name + emoji, role, event triggers, contextual heuristic); as they write, keystroke-level triggers fire, an LLM decision engine picks at most two whose heuristics match, and each produces an acknowledgement plus a question-style suggestion. Writers can ignore it, open it as inspiration, or have it edit the document.

What is here

A Partners lab page (frontend/src/pages/partners/), reachable from the Labs (···) menu. Configure partners, switch watching on, and tags appear when a heuristic is satisfied; clicking one opens the suggestion and a short follow-up conversation; ignoring one lets it fade after fifteen seconds.

Paper §4 Here
Partner customization
Long Pause / Sentence End / Text Selection triggers ✅ from polled snapshots, not keystrokes
Session goal ✅ as the existing document brief
Keystroke log fed to the decision engine ⚠️ coarse activity trace instead
Decision engine, at most two partners ✅ one model instead of two
Acknowledgement + question ✅ generated on click, not on activation
Tag aligned with the cursor ⚠️ fixed position in the panel
Ignoring / Inspiring
Executing — partner writes into the document ❌ deliberately not built

docs/proactive-partners-reproduction.md is the map and the challenge log.

The interesting problems

There are no keystrokes. The paper's triggers run on a live keystroke stream inside its own editor. A task pane's writer types into Word or Google Docs; Office.js offers only DocumentSelectionChanged, and Google Docs offers no selection event at all. The triggers are rebuilt from diffs of polled getDocContext() snapshots, and the 15-second keystroke log becomes a much coarser activity trace. Burst/pause rhythm, backspace runs, and the rest of what the keystroke-analysis literature the paper cites depends on are not observable here.

"Execute" is deliberately not implemented. docs/design/interface-concepts.md opens its covenant with "the writer's sentences are the writer's", and frontend/CLAUDE.md states that nothing in the add-in rewrites the writer's prose. This is the one place where a faithful reproduction and this project's stated values genuinely conflict, so it is flagged rather than quietly resolved — and it means this build cannot speak to the paper's findings about Executing. The machinery exists (EditorAPI.applyEdit), so it is a decision to revisit, not a capability gap.

The paper's prompts are not in the PDF. §4.5 says they are in supplementary materials; the arXiv appendix is references only. Every prompt here is reconstructed from prose, so a behavioural difference could be a prompt difference we cannot detect.

Two problems only running it found

Both are consequences of polling rather than listening, so they are evidence about the compromise rather than ordinary bugs.

An unwatched trigger was spending the cooldown. A writer whose only partner listened for Long pause received nothing, ever: every sentence they finished fired a Sentence end that no partner listened for, which produced no activation but started the 45-second cooldown and suppressed the pause behind it. The feature looked dead while every part of it worked. Fixed, with a regression test — but the general point stands and shapes any study run on this build: the cooldown is a scarce resource the noisiest trigger wins by default, so a writer with one sentence-end partner and one pause partner is not running two partners at equal odds. The paper has no cooldown and no such effect.

A polled observer has no history from before it starts. Its first snapshot is indistinguishable from "text just typed", so a writer who switched watching on and immediately typed a sentence and stopped got no pause — the burst had been absorbed into the baseline. Mitigated by sampling at once rather than on the first interval; the blind window shrinks to one host read but cannot close.

Verification

frontend/tests/partners-flows.spec.ts drives the whole loop in a browser — configure a partner, type, stop, tag appears, open it, get the suggestion and a follow-up. That spec is what found both problems above. 45 unit tests cover the state machine and the two response parsers. Typecheck, lint, and the full 284-test suite pass.

Not verified: any real host. Everything ran against the standalone Lexical editor, where reading the document is a function call. On Word each poll is a Word.run sync and on Google Docs an Apps Script round-trip; whether a 1.5-second poll is tolerable there is an open question.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LzPdByDv9KAZyJd6kmTYpR

kcarnold and others added 6 commits September 8, 2026 14:17
Before writing any code, work out what the paper's probe actually needs and
which of it a task-pane add-in can supply. Records eight challenges up front,
of which two are decisions rather than obstacles: Execute / "Help Me Write"
is omitted because it contradicts the repository's stated covenant, and the
paper's session goal is folded into the existing document brief.

The paper's prompts are said to be in supplementary materials that the arXiv
PDF does not contain, so every prompt in this reproduction is reconstructed
from the prose in §4.2-§4.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzPdByDv9KAZyJd6kmTYpR
…apshots

The paper's three triggers run on a live keystroke stream inside its own
editor. A task pane has no keystrokes on any host, so this derives the same
three from diffs of polled getDocContext() snapshots, and derives a coarse
activity trace to stand in for the 15-second keystroke log the decision
engine reads.

The state machine is a pure fold over (state, snapshot, config), so the
timing behaviour is tested by feeding it made-up timestamps rather than by
running clocks. It carries one parameter the paper does not have: a cooldown
between fired triggers, because polling makes triggers coincide in ways
keystrokes do not, and because each fired trigger costs a model call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzPdByDv9KAZyJd6kmTYpR
…torage

Reconstructs the paper's two LLM stages from its prose, since the prompts it
points at are in supplementary materials the arXiv PDF does not contain: an
engine that judges each partner's contextual heuristic against the moment and
selects at most two (preferring silence), and a suggestion of the paper's two
parts - an acknowledgement of what the writer is doing, then a question.

Both parsers are deliberately tolerant. A model that answers in prose, fences
its JSON, invents a partner id, or returns three partners should degrade to
something the writer can use, not throw on the writing path.

Partners persist as a document setting, which is the only store all three
hosts share; the paper's are per-user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzPdByDv9KAZyJd6kmTYpR
…ion cards

Wires the pieces into a page: configure partners, switch watching on, and
tags appear when the decision engine finds a heuristic satisfied. Clicking a
tag opens the suggestion and a short follow-up conversation; ignoring it lets
the tag fade after fifteen seconds, as in the paper.

Two of the paper's three engagement forms are here. The third, where the
partner writes into the document, is not, and the page says so on its face.

The suggestion is generated on click rather than on activation. The paper can
afford eager generation because it runs a cheap model for the decision and a
separate one for the suggestion; we have one model and noisier triggers, so
an unopened tag should not cost a generation.

Adds the page's log events at schema version 6. The interesting one is
partners_activated with activated: 0 - the engine choosing not to interrupt,
which is the outcome the paper reports as most common.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzPdByDv9KAZyJd6kmTYpR
…t touch

Running the repo-wide format script reformatted five unrelated files - the
installed prettier disagrees with what is committed on main. That drift is
real but it is not this branch's to fix, and it made the diff harder to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzPdByDv9KAZyJd6kmTYpR
Driving the page end-to-end found two failures that the unit tests could not,
because they feed the state machine snapshots rather than watching a writer
type.

A partner listening only for pauses never fired. Every sentence the writer
finished fired a sentence-end that no partner listened for; that produced no
activation, but it started the 45-second cooldown, and the pause behind it was
suppressed. The feature looked dead while every part of it worked. `observe`
now evaluates only the triggers some partner is listening for.

The second: a polled observer has no history from before it starts, so its
first snapshot becomes the baseline and anything typed in the gap before it is
invisible. Sampling the baseline the moment watching starts shrinks that blind
window from a poll interval to one host read; it cannot close it.

Adds a Playwright spec that configures a partner, types, stops, and opens the
resulting tag - the test that found both - and writes both up in the
reproduction log, along with what the cooldown means for a study run on this
build: it is a scarce resource the noisiest trigger wins by default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzPdByDv9KAZyJd6kmTYpR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant