Context
Raised in #59 comments: the current SharedMemory is a global namespace -- every agent sees every other agent's outputs, regardless of task dependencies. This causes inter-agent context pollution: Agent B's judgment gets influenced by Agent A's intermediate results even when the two tasks are completely independent.
This is a different problem from #59 (intra-agent context growth). #59 is about a single agent's conversation getting too long. This issue is about what context an agent sees from its teammates.
The root cause is an architectural one: our framework is default-allow -- agents see everything via SharedMemory unless we filter it. The correct model is default-deny: agents see nothing by default, the orchestrator explicitly passes only what's needed. With default-deny, context leakage is impossible by design, not by policy.
Where the leak happens today
buildTaskPrompt() in orchestrator.ts:474 calls sharedMem.getSummary() which dumps all entries from all agents into every task prompt:
// orchestrator.ts:484-488
const sharedMem = team.getSharedMemoryInstance()
if (sharedMem) {
const summary = await sharedMem.getSummary() // Returns EVERYTHING from ALL agents
if (summary) {
lines.push('', summary)
}
}
A team with 5 agents running 10 tasks means every agent prompt contains outputs from all prior tasks -- including unrelated ones. This:
- Pollutes judgment -- irrelevant context from unrelated tasks can bias an agent's output
- Wastes tokens -- agent pays for context it shouldn't need
- Makes debugging painful -- when an agent produces bad output, hard to tell which piece of injected context caused it
Design: Pure-function agents with dependency-only context
Core principle
Agents are pure functions: (task description + dependency results) -> result string. No shared mutable state. The orchestrator (not the agent) decides what context to inject.
Change 1: buildTaskPrompt() -- dependency results from TaskQueue, not SharedMemory
Before (current, leaky):
async function buildTaskPrompt(task: Task, team: Team): Promise<string> {
const lines = [`# Task: ${task.title}`, '', task.description]
// PROBLEM: injects ALL agents' outputs
const sharedMem = team.getSharedMemoryInstance()
if (sharedMem) {
const summary = await sharedMem.getSummary()
if (summary) lines.push('', summary)
}
// ...
}
After (pure-function model):
async function buildTaskPrompt(
task: Task,
team: Team,
queue: TaskQueue, // NEW: pass queue to read dependency results
): Promise<string> {
const lines = [`# Task: ${task.title}`, '', task.description]
// Only inject results from tasks this task EXPLICITLY depends on.
// No dependency = clean slate: zero prior context.
if (task.dependsOn && task.dependsOn.length > 0) {
const depResults: string[] = []
for (const depId of task.dependsOn) {
const depTask = queue.get(depId)
if (depTask?.status === 'completed' && depTask.result) {
depResults.push(
`### ${depTask.title} (by ${depTask.assignee ?? 'unknown'})\n${depTask.result}`
)
}
}
if (depResults.length > 0) {
lines.push('', '## Context from prerequisite tasks', '', ...depResults)
}
}
// Messages from team members -- keep as-is (explicit point-to-point)
if (task.assignee) {
const messages = team.getMessages(task.assignee)
if (messages.length > 0) {
lines.push('', '## Messages from team members')
for (const msg of messages) {
lines.push(`- **${msg.from}**: ${msg.content}`)
}
}
}
// NOTE: SharedMemory is NOT read here. Agent runs with a clean context.
return lines.join('\n')
}
Key behaviors:
- No dependencies -> agent gets ONLY its task description. Clean slate.
- Has dependencies -> agent gets task description + completed dependency task results (read from
queue.get(depId).result, not from SharedMemory).
- SharedMemory is never read during agent execution.
Change 2: executeQueue() -- pass TaskQueue to buildTaskPrompt
// orchestrator.ts, inside executeQueue(), around line 359
- const prompt = await buildTaskPrompt(task, team)
+ const prompt = await buildTaskPrompt(task, team, queue)
Change 3: SharedMemory write path -- keep for coordinator synthesis only
Currently orchestrator.ts:404-407 writes task results to SharedMemory. Keep this write, but document its changed role. SharedMemory is now orchestrator-internal storage used only by buildSynthesisPrompt() to give the coordinator a full view of all results. Agents never read it.
Change 4: buildSynthesisPrompt() -- unchanged (coordinator needs full view)
The coordinator's synthesis step already reads from the task queue AND SharedMemory. This is correct -- the coordinator needs full visibility to synthesize. No change needed here.
Change 5: TaskQueue.get() -- add method if missing
// task/queue.ts
get(taskId: string): Task | undefined {
return this.tasks.get(taskId)
}
Change 6: Optional memoryScope override on Task
interface Task {
// ...existing fields...
readonly memoryScope?: 'dependencies' | 'all'
}
Change 7: getSummary() -- add optional filter
async getSummary(filter?: { taskIds?: string[] }): Promise<string>
Change 8: runTasks() -- thread TaskQueue through
Ensure TaskQueue is threaded to executeQueue() -> buildTaskPrompt() correctly.
What this does NOT change
SharedMemory class, MessageBus, Team public API, runTeam() / runTasks() public API, task result persistence
Acceptance criteria
Non-goals
- Removing SharedMemory entirely (still needed for coordinator synthesis)
- Per-field read/write ACLs on SharedMemory (overkill)
- Process-level agent isolation (our agents run in-process, which is fine as long as context injection is controlled)
- Breaking changes to the public API
Context
Raised in #59 comments: the current
SharedMemoryis a global namespace -- every agent sees every other agent's outputs, regardless of task dependencies. This causes inter-agent context pollution: Agent B's judgment gets influenced by Agent A's intermediate results even when the two tasks are completely independent.This is a different problem from #59 (intra-agent context growth). #59 is about a single agent's conversation getting too long. This issue is about what context an agent sees from its teammates.
The root cause is an architectural one: our framework is default-allow -- agents see everything via SharedMemory unless we filter it. The correct model is default-deny: agents see nothing by default, the orchestrator explicitly passes only what's needed. With default-deny, context leakage is impossible by design, not by policy.
Where the leak happens today
buildTaskPrompt()inorchestrator.ts:474callssharedMem.getSummary()which dumps all entries from all agents into every task prompt:A team with 5 agents running 10 tasks means every agent prompt contains outputs from all prior tasks -- including unrelated ones. This:
Design: Pure-function agents with dependency-only context
Core principle
Change 1:
buildTaskPrompt()-- dependency results from TaskQueue, not SharedMemoryBefore (current, leaky):
After (pure-function model):
Key behaviors:
queue.get(depId).result, not from SharedMemory).Change 2:
executeQueue()-- pass TaskQueue to buildTaskPromptChange 3: SharedMemory write path -- keep for coordinator synthesis only
Currently
orchestrator.ts:404-407writes task results to SharedMemory. Keep this write, but document its changed role. SharedMemory is now orchestrator-internal storage used only bybuildSynthesisPrompt()to give the coordinator a full view of all results. Agents never read it.Change 4:
buildSynthesisPrompt()-- unchanged (coordinator needs full view)The coordinator's synthesis step already reads from the task queue AND SharedMemory. This is correct -- the coordinator needs full visibility to synthesize. No change needed here.
Change 5:
TaskQueue.get()-- add method if missingChange 6: Optional
memoryScopeoverride on TaskChange 7:
getSummary()-- add optional filterChange 8:
runTasks()-- thread TaskQueue throughEnsure TaskQueue is threaded to
executeQueue()->buildTaskPrompt()correctly.What this does NOT change
SharedMemoryclass,MessageBus,Teampublic API,runTeam()/runTasks()public API, task result persistenceAcceptance criteria
buildTaskPrompt()signature updated to acceptTaskQueuebuildTaskPrompt()default: reads dependency task results from TaskQueue, NOT SharedMemoryTaskQueue.get(taskId)method available (add if missing)memoryScope: 'all'opt-in on Task interface for full-visibility use casesmemoryScopethreaded throughrunTasks()task spec inputbuildSynthesisPrompt()unchanged (coordinator retains full visibility)getSummary()accepts optional{ taskIds?: string[] }filterNon-goals