feat: add customizable coordinator options for runTeam and enhance system prompt
This commit is contained in:
parent
dc8cbe0262
commit
30369b0597
|
|
@ -165,6 +165,7 @@ export type {
|
|||
// Orchestrator
|
||||
OrchestratorConfig,
|
||||
OrchestratorEvent,
|
||||
CoordinatorConfig,
|
||||
|
||||
// Trace
|
||||
TraceEventType,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
import type {
|
||||
AgentConfig,
|
||||
AgentRunResult,
|
||||
CoordinatorConfig,
|
||||
OrchestratorConfig,
|
||||
OrchestratorEvent,
|
||||
Task,
|
||||
|
|
@ -825,8 +826,13 @@ export class OpenMultiAgent {
|
|||
* @param team - A team created via {@link createTeam} (or `new Team(...)`).
|
||||
* @param goal - High-level natural-language goal for the team.
|
||||
*/
|
||||
async runTeam(team: Team, goal: string, options?: { abortSignal?: AbortSignal }): Promise<TeamRunResult> {
|
||||
async runTeam(
|
||||
team: Team,
|
||||
goal: string,
|
||||
options?: { abortSignal?: AbortSignal; coordinator?: CoordinatorConfig },
|
||||
): Promise<TeamRunResult> {
|
||||
const agentConfigs = team.getAgents()
|
||||
const coordinatorOverrides = options?.coordinator
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Short-circuit: skip coordinator for simple, single-action goals.
|
||||
|
|
@ -871,12 +877,17 @@ export class OpenMultiAgent {
|
|||
// ------------------------------------------------------------------
|
||||
const coordinatorConfig: AgentConfig = {
|
||||
name: 'coordinator',
|
||||
model: this.config.defaultModel,
|
||||
provider: this.config.defaultProvider,
|
||||
baseURL: this.config.defaultBaseURL,
|
||||
apiKey: this.config.defaultApiKey,
|
||||
systemPrompt: this.buildCoordinatorSystemPrompt(agentConfigs),
|
||||
maxTurns: 3,
|
||||
model: coordinatorOverrides?.model ?? this.config.defaultModel,
|
||||
provider: coordinatorOverrides?.provider ?? this.config.defaultProvider,
|
||||
baseURL: coordinatorOverrides?.baseURL ?? this.config.defaultBaseURL,
|
||||
apiKey: coordinatorOverrides?.apiKey ?? this.config.defaultApiKey,
|
||||
systemPrompt: this.buildCoordinatorPrompt(agentConfigs, coordinatorOverrides),
|
||||
maxTurns: coordinatorOverrides?.maxTurns ?? 3,
|
||||
maxTokens: coordinatorOverrides?.maxTokens,
|
||||
temperature: coordinatorOverrides?.temperature,
|
||||
tools: coordinatorOverrides?.tools,
|
||||
loopDetection: coordinatorOverrides?.loopDetection,
|
||||
timeoutMs: coordinatorOverrides?.timeoutMs,
|
||||
}
|
||||
|
||||
const decompositionPrompt = this.buildDecompositionPrompt(goal, agentConfigs)
|
||||
|
|
@ -1121,6 +1132,47 @@ export class OpenMultiAgent {
|
|||
|
||||
/** Build the system prompt given to the coordinator agent. */
|
||||
private buildCoordinatorSystemPrompt(agents: AgentConfig[]): string {
|
||||
return [
|
||||
'You are a task coordinator responsible for decomposing high-level goals',
|
||||
'into concrete, actionable tasks and assigning them to the right team members.',
|
||||
'',
|
||||
this.buildCoordinatorRosterSection(agents),
|
||||
'',
|
||||
this.buildCoordinatorOutputFormatSection(),
|
||||
'',
|
||||
this.buildCoordinatorSynthesisSection(),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Build coordinator system prompt with optional caller overrides. */
|
||||
private buildCoordinatorPrompt(agents: AgentConfig[], config?: CoordinatorConfig): string {
|
||||
if (config?.systemPrompt) {
|
||||
return [
|
||||
config.systemPrompt,
|
||||
'',
|
||||
this.buildCoordinatorRosterSection(agents),
|
||||
'',
|
||||
this.buildCoordinatorOutputFormatSection(),
|
||||
'',
|
||||
this.buildCoordinatorSynthesisSection(),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const base = this.buildCoordinatorSystemPrompt(agents)
|
||||
if (!config?.instructions) {
|
||||
return base
|
||||
}
|
||||
|
||||
return [
|
||||
base,
|
||||
'',
|
||||
'## Additional Instructions',
|
||||
config.instructions,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Build the coordinator team roster section. */
|
||||
private buildCoordinatorRosterSection(agents: AgentConfig[]): string {
|
||||
const roster = agents
|
||||
.map(
|
||||
(a) =>
|
||||
|
|
@ -1129,12 +1181,14 @@ export class OpenMultiAgent {
|
|||
.join('\n')
|
||||
|
||||
return [
|
||||
'You are a task coordinator responsible for decomposing high-level goals',
|
||||
'into concrete, actionable tasks and assigning them to the right team members.',
|
||||
'',
|
||||
'## Team Roster',
|
||||
roster,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Build the coordinator JSON output-format section. */
|
||||
private buildCoordinatorOutputFormatSection(): string {
|
||||
return [
|
||||
'## Output Format',
|
||||
'When asked to decompose a goal, respond ONLY with a JSON array of task objects.',
|
||||
'Each task must have:',
|
||||
|
|
@ -1145,7 +1199,12 @@ export class OpenMultiAgent {
|
|||
'',
|
||||
'Wrap the JSON in a ```json code fence.',
|
||||
'Do not include any text outside the code fence.',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Build the coordinator synthesis guidance section. */
|
||||
private buildCoordinatorSynthesisSection(): string {
|
||||
return [
|
||||
'## When synthesising results',
|
||||
'You will be given completed task outputs and asked to synthesise a final answer.',
|
||||
'Write a clear, comprehensive response that addresses the original goal.',
|
||||
|
|
|
|||
33
src/types.ts
33
src/types.ts
|
|
@ -422,6 +422,39 @@ export interface OrchestratorConfig {
|
|||
readonly onApproval?: (completedTasks: readonly Task[], nextTasks: readonly Task[]) => Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional overrides for the temporary coordinator agent created by `runTeam`.
|
||||
*
|
||||
* All fields are optional. Unset fields fall back to orchestrator defaults
|
||||
* (or coordinator built-in defaults where applicable).
|
||||
*/
|
||||
export interface CoordinatorConfig {
|
||||
/** Coordinator model. Defaults to `OrchestratorConfig.defaultModel`. */
|
||||
readonly model?: string
|
||||
readonly provider?: 'anthropic' | 'copilot' | 'grok' | 'openai' | 'gemini'
|
||||
readonly baseURL?: string
|
||||
readonly apiKey?: string
|
||||
/**
|
||||
* Full system prompt override. When set, this replaces the default
|
||||
* coordinator preamble and decomposition guidance.
|
||||
*
|
||||
* Team roster, output format, and synthesis sections are still appended.
|
||||
*/
|
||||
readonly systemPrompt?: string
|
||||
/**
|
||||
* Additional instructions appended to the default coordinator prompt.
|
||||
* Ignored when `systemPrompt` is provided.
|
||||
*/
|
||||
readonly instructions?: string
|
||||
readonly maxTurns?: number
|
||||
readonly maxTokens?: number
|
||||
readonly temperature?: number
|
||||
/** Tool names available to the coordinator. */
|
||||
readonly tools?: readonly string[]
|
||||
readonly loopDetection?: LoopDetectionConfig
|
||||
readonly timeoutMs?: number
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trace events — lightweight observability spans
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ function createMockAdapter(responses: string[]): LLMAdapter {
|
|||
* We need to do this at the module level because Agent calls createAdapter internally.
|
||||
*/
|
||||
let mockAdapterResponses: string[] = []
|
||||
let capturedChatOptions: LLMChatOptions[] = []
|
||||
|
||||
vi.mock('../src/llm/adapter.js', () => ({
|
||||
createAdapter: async () => {
|
||||
|
|
@ -49,6 +50,7 @@ vi.mock('../src/llm/adapter.js', () => ({
|
|||
return {
|
||||
name: 'mock',
|
||||
async chat(_msgs: LLMMessage[], options: LLMChatOptions): Promise<LLMResponse> {
|
||||
capturedChatOptions.push(options)
|
||||
const text = mockAdapterResponses[callIndex] ?? 'default mock response'
|
||||
callIndex++
|
||||
return {
|
||||
|
|
@ -94,6 +96,7 @@ function teamCfg(agents?: AgentConfig[]): TeamConfig {
|
|||
describe('OpenMultiAgent', () => {
|
||||
beforeEach(() => {
|
||||
mockAdapterResponses = []
|
||||
capturedChatOptions = []
|
||||
})
|
||||
|
||||
describe('createTeam', () => {
|
||||
|
|
@ -237,6 +240,120 @@ describe('OpenMultiAgent', () => {
|
|||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('supports coordinator model override without affecting workers', async () => {
|
||||
mockAdapterResponses = [
|
||||
'```json\n[{"title": "Research", "description": "Research", "assignee": "worker-a"}]\n```',
|
||||
'worker output',
|
||||
'final synthesis',
|
||||
]
|
||||
|
||||
const oma = new OpenMultiAgent({
|
||||
defaultModel: 'expensive-model',
|
||||
defaultProvider: 'openai',
|
||||
})
|
||||
const team = oma.createTeam('t', teamCfg([
|
||||
{ ...agentConfig('worker-a'), model: 'worker-model' },
|
||||
]))
|
||||
|
||||
const result = await oma.runTeam(team, 'First research the topic, then synthesize findings', {
|
||||
coordinator: { model: 'cheap-model' },
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(capturedChatOptions.length).toBe(3)
|
||||
expect(capturedChatOptions[0]?.model).toBe('cheap-model')
|
||||
expect(capturedChatOptions[1]?.model).toBe('worker-model')
|
||||
expect(capturedChatOptions[2]?.model).toBe('cheap-model')
|
||||
})
|
||||
|
||||
it('appends coordinator.instructions to the default system prompt', async () => {
|
||||
mockAdapterResponses = [
|
||||
'```json\n[{"title": "Plan", "description": "Plan", "assignee": "worker-a"}]\n```',
|
||||
'done',
|
||||
'final',
|
||||
]
|
||||
|
||||
const oma = new OpenMultiAgent({
|
||||
defaultModel: 'mock-model',
|
||||
defaultProvider: 'openai',
|
||||
})
|
||||
const team = oma.createTeam('t', teamCfg([
|
||||
{ ...agentConfig('worker-a'), model: 'worker-model' },
|
||||
]))
|
||||
|
||||
await oma.runTeam(team, 'First implement, then verify', {
|
||||
coordinator: {
|
||||
instructions: 'Always create a testing task after implementation tasks.',
|
||||
},
|
||||
})
|
||||
|
||||
const coordinatorPrompt = capturedChatOptions[0]?.systemPrompt ?? ''
|
||||
expect(coordinatorPrompt).toContain('You are a task coordinator responsible')
|
||||
expect(coordinatorPrompt).toContain('## Additional Instructions')
|
||||
expect(coordinatorPrompt).toContain('Always create a testing task after implementation tasks.')
|
||||
})
|
||||
|
||||
it('uses coordinator.systemPrompt override while still appending required sections', async () => {
|
||||
mockAdapterResponses = [
|
||||
'```json\n[{"title": "Plan", "description": "Plan", "assignee": "worker-a"}]\n```',
|
||||
'done',
|
||||
'final',
|
||||
]
|
||||
|
||||
const oma = new OpenMultiAgent({
|
||||
defaultModel: 'mock-model',
|
||||
defaultProvider: 'openai',
|
||||
})
|
||||
const team = oma.createTeam('t', teamCfg([
|
||||
{ ...agentConfig('worker-a'), model: 'worker-model' },
|
||||
]))
|
||||
|
||||
await oma.runTeam(team, 'First implement, then verify', {
|
||||
coordinator: {
|
||||
systemPrompt: 'You are a custom coordinator for monorepo planning.',
|
||||
},
|
||||
})
|
||||
|
||||
const coordinatorPrompt = capturedChatOptions[0]?.systemPrompt ?? ''
|
||||
expect(coordinatorPrompt).toContain('You are a custom coordinator for monorepo planning.')
|
||||
expect(coordinatorPrompt).toContain('## Team Roster')
|
||||
expect(coordinatorPrompt).toContain('## Output Format')
|
||||
expect(coordinatorPrompt).toContain('## When synthesising results')
|
||||
expect(coordinatorPrompt).not.toContain('You are a task coordinator responsible')
|
||||
})
|
||||
|
||||
it('applies advanced coordinator options (maxTokens, temperature, tools)', async () => {
|
||||
mockAdapterResponses = [
|
||||
'```json\n[{"title": "Inspect", "description": "Inspect", "assignee": "worker-a"}]\n```',
|
||||
'worker output',
|
||||
'final synthesis',
|
||||
]
|
||||
|
||||
const oma = new OpenMultiAgent({
|
||||
defaultModel: 'mock-model',
|
||||
defaultProvider: 'openai',
|
||||
})
|
||||
const team = oma.createTeam('t', teamCfg([
|
||||
{ ...agentConfig('worker-a'), model: 'worker-model' },
|
||||
]))
|
||||
|
||||
await oma.runTeam(team, 'First inspect project, then produce output', {
|
||||
coordinator: {
|
||||
maxTurns: 5,
|
||||
maxTokens: 1234,
|
||||
temperature: 0,
|
||||
tools: ['file_read'],
|
||||
timeoutMs: 1500,
|
||||
loopDetection: { maxRepetitions: 2, loopDetectionWindow: 3 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(capturedChatOptions[0]?.maxTokens).toBe(1234)
|
||||
expect(capturedChatOptions[0]?.temperature).toBe(0)
|
||||
expect(capturedChatOptions[0]?.tools).toBeDefined()
|
||||
expect(capturedChatOptions[0]?.tools?.map((t) => t.name)).toContain('file_read')
|
||||
})
|
||||
})
|
||||
|
||||
describe('config defaults', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue