Skip to content

feat: SharedStore for parallel workflow agent communication (#47)#48

Merged
QuintinShaw merged 11 commits into
QuintinShaw:mainfrom
gr3enarr0w:feat/shared-store-47
Jul 3, 2026
Merged

feat: SharedStore for parallel workflow agent communication (#47)#48
QuintinShaw merged 11 commits into
QuintinShaw:mainfrom
gr3enarr0w:feat/shared-store-47

Conversation

@gr3enarr0w

@gr3enarr0w gr3enarr0w commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Parallel agents in a workflow run had no way to share intermediate state. This adds a SharedStore (in-memory Map scoped per run) with store_put/store_get tools injected into every agent, plus per-agent delta journaling so shared state replays correctly on resume.

New: src/shared-store.ts

  • SharedStoreMap<string, unknown> with put, trackPut, get, has, snapshot, commitDelta, applyDelta, restore, dispose
  • createSharedStoreTools(store) — plain (non-journaled) [store_put, store_get] bound to a store; exported for host/external use
  • createAgentStoreTools(store, deltaKey) — the per-agent journaled variant used internally by the runtime; attributes each write to a run-unique deltaKey so it can be replayed on resume
  • store_put description documents last-write-wins semantics for parallel agents

Modified: src/workflow.ts

  • One SharedStore per top-level runWorkflow call; nested workflow() calls inherit it via WorkflowRunOptions.sharedStore
  • createAgentStoreTools(store, deltaKey) injected into every agent via systemTools (bypasses the toolNames allowlist)
  • Per-agent write delta (storeDelta) captured in each journal entry; on cache-hit replay store.applyDelta(delta) re-applies it additively in callSeq order, so parallel-agent writes reconstruct correctly regardless of original completion order
  • The store is disposed only by the run that created it (nested runs don't tear down the parent's store)

Modified: src/agent.ts

  • systemTools?: ToolDefinition[] added to AgentRunOptions — tools injected after the allowlist/denylist filter, so store tools reach every agent even under a restrictive agentType

Exported from src/index.ts: SharedStore, createSharedStoreTools

Maintainer fixes pushed to this branch

  • Delta-key collision fix: a nested workflow() shares the parent's store but restarts its own callSeq at 0, so a parent agent and a nested-run agent could both land on callIndex 0 and clobber each other in agentDeltas. Deltas are now keyed by ${runId}:${callIndex} — unique across the shared store's lifetime since every run (including each nested run, ${runId}-nested{depth}) has a distinct runId.
  • Strengthened tests: the isolation test now actually writes in run 1 and asserts the miss in run 2; added an allowlist-bypass test (restrictive agentType still receives store_put/store_get) and a nested-collision regression test.

Notes / caveats

  • Writes via the public SharedStore.put() are not journaled — only the per-agent tool path (trackPut) is tracked and replayed. Host/script code calling put() directly bypasses resume.
  • Replay applies deltas in callSeq order, so which parallel writer "wins" a contested key is deterministic within a run but can flip across a resume — acceptable under last-write-wins.

Closes #47

Test plan

  • Agent A calls store_put("key", val) → agent B (same run) retrieves it with store_get("key") (real-pi verified)
  • Store is not shared between two separate runWorkflow calls
  • Run resume replays store state correctly at each cache-hit boundary (additive delta replay)
  • store_get on missing key returns null (not error)
  • Restrictive agentType allowlist still receives the store tools
  • Nested workflow() concurrent writes don't collide in the journal

@QuintinShaw

Copy link
Copy Markdown
Owner

Nice structure, and the live path (per-run scoping, dispose timing, tool injection) checks out. But the resume/replay path — the load-bearing part of the design — has a blocking correctness bug:

restore() does map.clear() + full replace, and each agent's journaled storeSnapshot is a whole-Map view captured at completion time. Parallel agents finish out of callSeq order, so on resume the restores replay in index order and the highest-index snapshot wins by replacement — wiping keys that sibling agents wrote but that snapshot never held. Net: resuming a run with a parallel fan-out can silently drop committed writes, and a downstream agent sees a store state that never existed on the original run. It reproduces with the ordinary "write result, then finish" pattern, not an edge case.

Whole-Map snapshot + replace-on-restore is fundamentally incompatible with out-of-order parallel completion. Cleanest fix: journal a per-agent write delta (just the keys that agent set) and apply deltas additively in callSeq order on replay — that also makes the journaled value deterministic (today two resumes of the same run can restore different prefixes). structuredClone in snapshot() is worth adding alongside.

Two more before re-review:

  • SharedStore / createSharedStoreTools aren't exported from src/index.ts (the body says they are) — so feat: shared memory store for parallel workflow agents #47's "types exported for host app access" isn't met yet.
  • No tests landed despite the 5-point plan; the resume-under-fan-out and cross-run isolation cases both fail against the current code and would pin the fix.

Happy to re-review once the snapshot/restore semantics are reworked.

Clark Everson and others added 5 commits June 28, 2026 11:49
Adds SharedStore (in-memory Map per runId) with store_put/store_get
tools injected into every agent in a workflow run, enabling parallel
agents to share intermediate state without going through the parent.

Closes QuintinShaw#47
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rdering

Replace the full-map storeSnapshot/restore approach with per-agent write deltas
so parallel agents finishing out of callSeq order no longer lose each other's
writes on resume.

Key changes:
- SharedStore.trackPut(key, value, callIndex): records puts in a per-agent delta
- SharedStore.commitDelta(callIndex): extracts and clears the agent's delta
- SharedStore.applyDelta(delta): additive replay without clearing other keys
- SharedStore.snapshot(): now returns a structuredClone (deep copy)
- createAgentStoreTools(store, callIndex): per-agent tools that call trackPut
- workflow.ts: use createAgentStoreTools per call, journal storeDelta, replay
  with applyDelta instead of storeSnapshot/restore
- src/index.ts: export SharedStore and createSharedStoreTools for consumers
- tests/shared-store.test.ts: unit tests for delta tracking, cross-run
  isolation, and resume-under-fan-out integration test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Preparing review...

3 similar comments
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Preparing review...

@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Preparing review...

@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Preparing review...

@gr3enarr0w

gr3enarr0w commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

PR Reviewer Guide 🔍

(Review updated until commit 76b130d)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ✅

47 - Fully compliant

Compliant requirements:

  • store_put / store_get tools available to all agents in a workflow run
  • Parallel agents can read each other's writes (with last-write-wins semantics)
  • Store is scoped per run — no cross-run leakage
  • Store contents included in journal entries for replay consistency
  • TypeScript types exported for host app access
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

- Remove unused `createSharedStoreTools` import (noUnusedImports)
- Replace bracket notation with dot notation (useLiteralKeys)
- Replace banned `Function` type with explicit signature (noBannedTypes)
- Sort import names and index.ts exports (organizeImports)
- Format test file to satisfy formatter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gr3enarr0w gr3enarr0w force-pushed the feat/shared-store-47 branch from e079886 to 32dfff0 Compare June 28, 2026 21:31
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Persistent review updated to latest commit 32dfff0

1 similar comment
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Persistent review updated to latest commit 32dfff0

Clark Everson and others added 3 commits June 28, 2026 17:52
…store_get

Storing `undefined` explicitly would be treated as a missing key under the old
check. Using `Map.has()` correctly distinguishes a present-but-undefined value
from an absent key in both createSharedStoreTools and createAgentStoreTools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two tests in shared-store.test.ts were failing:

1. YAML frontmatter (`---`) was used in inline script strings but
   parseWorkflowScript expects `export const meta = { ... }` as the
   first statement. Acorn choked on `---` with SyntaxError.

2. The resume-replay test populated resumeJournal with ALL agent
   entries including the get agents, so the gets were served as cache
   hits and the mock agent never ran — leaving writeCalls.alpha undefined.
   Fix: include only put agents (non-empty storeDelta) so gets run live
   against the store rebuilt from deltas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Persistent review updated to latest commit 76b130d

@QuintinShaw

Copy link
Copy Markdown
Owner

The mechanism itself checks out — the tools are genuinely wired through systemTools past the allowlist, and the per-agent delta journaling is a better design than the snapshot approach the PR body describes. A few things before merge:

  1. Real bug: nested workflow() calls restart callSeq at 0 but share the parent's store, so agentDeltas keyed by bare callIndex collides between a parent agent and a nested-run agent running concurrently — whichever commits first steals the other's writes in the journal. Key deltas by something run-unique (e.g. runId:callIndex).
  2. The "isolation between two runWorkflow calls" test never writes to the store and never asserts the found flag — it only checks results.length === 2. Please make it actually put in run 1 and assert the miss in run 2. A test that a restrictive agentType tools allowlist still receives store_put/store_get would also cover the headline feature.
  3. The PR body describes the first iteration, not the branch: it says storeSnapshot/store.restore on cache-hit (it's storeDelta/applyDelta now), createSharedStoreTools in systemTools (it's createAgentStoreTools), and sharedStore on AgentOptions in agent.ts (it lives on WorkflowRunOptions). Please update it — reviewers read the description first.

Worth documenting: writes via the public put() aren't journaled (only the tool path is), and replay applies deltas in callIndex order, so which parallel writer "wins" a contested key can flip across resume. Both acceptable under last-write-wins, but they should be stated.

Resolves conflict in src/agent.ts by keeping both main's per-run
modelRegistry override (systemTools field from PR QuintinShaw#48 sits alongside it).
@QuintinShaw

Copy link
Copy Markdown
Owner

Picked this up and pushed the fixes to your branch. The delta journaling is a nice design; the one real issue was the collision I flagged — a nested workflow() shares the parent store but restarts callSeq at 0, so a parent agent and a nested-run agent could both land on callIndex 0 and clobber each other in agentDeltas. Keyed the deltas by ${runId}:${callIndex} instead (every run, including each nested run, has a distinct runId), unique across the shared store's lifetime. Resume/replay is unchanged since it works off the journaled storeDelta payload, not the internal keying. Also strengthened the isolation test to actually write in run 1 and assert the miss in run 2, added an allowlist-bypass test (restrictive agentType still gets store_put/store_get) and a nested-collision regression test. Tidied the PR description too — a few internal names in it had drifted from the final branch. tsc clean, store tests green, real-pi smoke (two agents communicating through the store) passed. Thanks!

@QuintinShaw QuintinShaw merged commit e2ea963 into QuintinShaw:main Jul 3, 2026
1 check passed
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.

feat: shared memory store for parallel workflow agents

2 participants