Compare commits

..

1 Commits

Author SHA1 Message Date
NamelessNATM ac8ad8d593
Merge fb6051146f into f1c7477a26 2026-04-09 04:00:49 +08:00
9 changed files with 12 additions and 151 deletions

View File

@ -4,8 +4,6 @@
* Demonstrates how to define tasks with explicit dependency chains
* (design implement test review) using runTasks(). The TaskQueue
* automatically blocks downstream tasks until their dependencies complete.
* Prompt context is dependency-scoped by default: each task sees only its own
* description plus direct dependency results (not unrelated team outputs).
*
* Run:
* npx tsx examples/03-task-pipeline.ts
@ -118,7 +116,6 @@ const tasks: Array<{
description: string
assignee?: string
dependsOn?: string[]
memoryScope?: 'dependencies' | 'all'
}> = [
{
title: 'Design: URL shortener data model',
@ -165,9 +162,6 @@ Produce a structured code review with sections:
- Verdict: SHIP or NEEDS WORK`,
assignee: 'reviewer',
dependsOn: ['Implement: URL shortener'], // runs in parallel with Test after Implement completes
// Optional override: reviewers can opt into full shared memory when needed.
// Remove this line to keep strict dependency-only context.
memoryScope: 'all',
},
]

View File

@ -124,18 +124,8 @@ export class SharedMemory {
* - plan: Implement feature X using const type params
* ```
*/
async getSummary(filter?: { taskIds?: string[] }): Promise<string> {
let all = await this.store.list()
if (filter?.taskIds && filter.taskIds.length > 0) {
const taskIds = new Set(filter.taskIds)
all = all.filter((entry) => {
const slashIdx = entry.key.indexOf('/')
const localKey = slashIdx === -1 ? entry.key : entry.key.slice(slashIdx + 1)
if (!localKey.startsWith('task:') || !localKey.endsWith(':result')) return false
const taskId = localKey.slice('task:'.length, localKey.length - ':result'.length)
return taskIds.has(taskId)
})
}
async getSummary(): Promise<string> {
const all = await this.store.list()
if (all.length === 0) return ''
// Group entries by agent name.

View File

@ -329,10 +329,6 @@ interface ParsedTaskSpec {
description: string
assignee?: string
dependsOn?: string[]
memoryScope?: 'dependencies' | 'all'
maxRetries?: number
retryDelayMs?: number
retryBackoff?: number
}
/**
@ -371,10 +367,6 @@ function parseTaskSpecs(raw: string): ParsedTaskSpec[] | null {
dependsOn: Array.isArray(obj['dependsOn'])
? (obj['dependsOn'] as unknown[]).filter((x): x is string => typeof x === 'string')
: undefined,
memoryScope: obj['memoryScope'] === 'all' ? 'all' : undefined,
maxRetries: typeof obj['maxRetries'] === 'number' ? obj['maxRetries'] : undefined,
retryDelayMs: typeof obj['retryDelayMs'] === 'number' ? obj['retryDelayMs'] : undefined,
retryBackoff: typeof obj['retryBackoff'] === 'number' ? obj['retryBackoff'] : undefined,
})
}
@ -553,8 +545,8 @@ async function executeQueue(
data: task,
} satisfies OrchestratorEvent)
// Build the prompt: task description + dependency-only context by default.
const prompt = await buildTaskPrompt(task, team, queue)
// Build the prompt: inject shared memory context + task description
const prompt = await buildTaskPrompt(task, team)
// Trace + abort + team tool context (delegate_to_agent)
const traceBase: Partial<RunOptions> = {
@ -699,37 +691,22 @@ async function executeQueue(
*
* Injects:
* - Task title and description
* - Direct dependency task results by default (clean slate when none)
* - Optional full shared-memory context when `task.memoryScope === 'all'`
* - Dependency results from shared memory (if available)
* - Any messages addressed to this agent from the team bus
*/
async function buildTaskPrompt(task: Task, team: Team, queue: TaskQueue): Promise<string> {
async function buildTaskPrompt(task: Task, team: Team): Promise<string> {
const lines: string[] = [
`# Task: ${task.title}`,
'',
task.description,
]
if (task.memoryScope === 'all') {
// Explicit opt-in for full visibility (legacy/shared-memory behavior).
const sharedMem = team.getSharedMemoryInstance()
if (sharedMem) {
const summary = await sharedMem.getSummary()
if (summary) {
lines.push('', summary)
}
}
} else if (task.dependsOn && task.dependsOn.length > 0) {
// Default-deny: inject only explicit prerequisite outputs.
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)
// Inject shared memory summary so the agent sees its teammates' work
const sharedMem = team.getSharedMemoryInstance()
if (sharedMem) {
const summary = await sharedMem.getSummary()
if (summary) {
lines.push('', summary)
}
}
@ -1161,7 +1138,6 @@ export class OpenMultiAgent {
description: string
assignee?: string
dependsOn?: string[]
memoryScope?: 'dependencies' | 'all'
maxRetries?: number
retryDelayMs?: number
retryBackoff?: number
@ -1178,7 +1154,6 @@ export class OpenMultiAgent {
description: t.description,
assignee: t.assignee,
dependsOn: t.dependsOn,
memoryScope: t.memoryScope,
maxRetries: t.maxRetries,
retryDelayMs: t.retryDelayMs,
retryBackoff: t.retryBackoff,
@ -1400,7 +1375,6 @@ export class OpenMultiAgent {
*/
private loadSpecsIntoQueue(
specs: ReadonlyArray<ParsedTaskSpec & {
memoryScope?: 'dependencies' | 'all'
maxRetries?: number
retryDelayMs?: number
retryBackoff?: number
@ -1421,7 +1395,6 @@ export class OpenMultiAgent {
assignee: spec.assignee && agentNames.has(spec.assignee)
? spec.assignee
: undefined,
memoryScope: spec.memoryScope,
maxRetries: spec.maxRetries,
retryDelayMs: spec.retryDelayMs,
retryBackoff: spec.retryBackoff,

View File

@ -289,11 +289,6 @@ export class TaskQueue {
return this.list().filter((t) => t.status === status)
}
/** Returns a task by ID, if present. */
get(taskId: string): Task | undefined {
return this.tasks.get(taskId)
}
/**
* Returns `true` when every task in the queue has reached a terminal state
* (`'completed'`, `'failed'`, or `'skipped'`), **or** the queue is empty.

View File

@ -31,7 +31,6 @@ export function createTask(input: {
description: string
assignee?: string
dependsOn?: string[]
memoryScope?: 'dependencies' | 'all'
maxRetries?: number
retryDelayMs?: number
retryBackoff?: number
@ -44,7 +43,6 @@ export function createTask(input: {
status: 'pending' as TaskStatus,
assignee: input.assignee,
dependsOn: input.dependsOn ? [...input.dependsOn] : undefined,
memoryScope: input.memoryScope,
result: undefined,
createdAt: now,
updatedAt: now,

View File

@ -373,12 +373,6 @@ export interface Task {
assignee?: string
/** IDs of tasks that must complete before this one can start. */
dependsOn?: readonly string[]
/**
* Controls what prior team context is injected into this task's prompt.
* - `dependencies` (default): only direct dependency task results
* - `all`: full shared-memory summary
*/
readonly memoryScope?: 'dependencies' | 'all'
result?: string
readonly createdAt: Date
updatedAt: Date

View File

@ -43,7 +43,6 @@ function createMockAdapter(responses: string[]): LLMAdapter {
*/
let mockAdapterResponses: string[] = []
let capturedChatOptions: LLMChatOptions[] = []
let capturedPrompts: string[] = []
vi.mock('../src/llm/adapter.js', () => ({
createAdapter: async () => {
@ -52,12 +51,6 @@ vi.mock('../src/llm/adapter.js', () => ({
name: 'mock',
async chat(_msgs: LLMMessage[], options: LLMChatOptions): Promise<LLMResponse> {
capturedChatOptions.push(options)
const lastUser = [..._msgs].reverse().find((m) => m.role === 'user')
const prompt = (lastUser?.content ?? [])
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
.map((b) => b.text)
.join('\n')
capturedPrompts.push(prompt)
const text = mockAdapterResponses[callIndex] ?? 'default mock response'
callIndex++
return {
@ -104,7 +97,6 @@ describe('OpenMultiAgent', () => {
beforeEach(() => {
mockAdapterResponses = []
capturedChatOptions = []
capturedPrompts = []
})
describe('createTeam', () => {
@ -206,67 +198,6 @@ describe('OpenMultiAgent', () => {
expect(result.success).toBe(true)
})
it('uses a clean slate for tasks without dependencies', async () => {
mockAdapterResponses = ['alpha done', 'beta done']
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
const team = oma.createTeam('t', teamCfg())
await oma.runTasks(team, [
{ title: 'Independent A', description: 'Do independent A', assignee: 'worker-a' },
{ title: 'Independent B', description: 'Do independent B', assignee: 'worker-b' },
])
const workerPrompts = capturedPrompts.slice(0, 2)
expect(workerPrompts[0]).toContain('# Task: Independent A')
expect(workerPrompts[1]).toContain('# Task: Independent B')
expect(workerPrompts[0]).not.toContain('## Shared Team Memory')
expect(workerPrompts[1]).not.toContain('## Shared Team Memory')
expect(workerPrompts[0]).not.toContain('## Context from prerequisite tasks')
expect(workerPrompts[1]).not.toContain('## Context from prerequisite tasks')
})
it('injects only dependency results into dependent task prompts', async () => {
mockAdapterResponses = ['first output', 'second output']
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
const team = oma.createTeam('t', teamCfg())
await oma.runTasks(team, [
{ title: 'First', description: 'Produce first', assignee: 'worker-a' },
{ title: 'Second', description: 'Use first', assignee: 'worker-b', dependsOn: ['First'] },
])
const secondPrompt = capturedPrompts[1] ?? ''
expect(secondPrompt).toContain('## Context from prerequisite tasks')
expect(secondPrompt).toContain('### First (by worker-a)')
expect(secondPrompt).toContain('first output')
expect(secondPrompt).not.toContain('## Shared Team Memory')
})
it('supports memoryScope all opt-in for full shared memory visibility', async () => {
mockAdapterResponses = ['writer output', 'reader output']
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
const team = oma.createTeam('t', teamCfg())
await oma.runTasks(team, [
{ title: 'Write', description: 'Write something', assignee: 'worker-a' },
{
title: 'Read all',
description: 'Read everything',
assignee: 'worker-b',
memoryScope: 'all',
dependsOn: ['Write'],
},
])
const secondPrompt = capturedPrompts[1] ?? ''
expect(secondPrompt).toContain('## Shared Team Memory')
expect(secondPrompt).toContain('task:')
expect(secondPrompt).not.toContain('## Context from prerequisite tasks')
})
})
describe('runTeam', () => {

View File

@ -107,19 +107,6 @@ describe('SharedMemory', () => {
expect(summary).toContain('…')
})
it('filters summary to only requested task IDs', async () => {
const mem = new SharedMemory()
await mem.write('alice', 'task:t1:result', 'output 1')
await mem.write('bob', 'task:t2:result', 'output 2')
await mem.write('alice', 'notes', 'not a task result')
const summary = await mem.getSummary({ taskIds: ['t2'] })
expect(summary).toContain('### bob')
expect(summary).toContain('task:t2:result: output 2')
expect(summary).not.toContain('task:t1:result: output 1')
expect(summary).not.toContain('notes: not a task result')
})
// -------------------------------------------------------------------------
// listAll
// -------------------------------------------------------------------------

View File

@ -27,7 +27,6 @@ describe('TaskQueue', () => {
q.add(task('a'))
expect(q.list()).toHaveLength(1)
expect(q.list()[0].id).toBe('a')
expect(q.get('a')?.title).toBe('a')
})
it('fires task:ready for a task with no dependencies', () => {