diff --git a/packages/core/src/__tests__/builder-agent-round-trip.test.ts b/packages/core/src/__tests__/builder-agent-round-trip.test.ts index cdb0766..54f6c01 100644 --- a/packages/core/src/__tests__/builder-agent-round-trip.test.ts +++ b/packages/core/src/__tests__/builder-agent-round-trip.test.ts @@ -76,11 +76,33 @@ describe('WorkflowBuilder.agent()', () => { }); }); - it('keeps persona-only options unrepresentable in the builder API', () => { - // @ts-expect-error Persona agents cannot use non-interactive CLI presets. - workflow('invalid-persona-preset').agent('integrations', { persona: 'nango-integrations', preset: 'worker' }); - // @ts-expect-error Persona agents are always interactive. - workflow('invalid-persona-interactive').agent('integrations', { persona: 'nango-integrations', interactive: false }); + it('rejects persona-only options at runtime as well as in the builder API', () => { + expect(() => + // @ts-expect-error Persona agents cannot use non-interactive CLI presets. + workflow('invalid-persona-preset').agent('integrations', { + persona: 'nango-integrations', + preset: 'worker', + }) + ).toThrow('Persona agent "integrations" cannot define'); + expect(() => + // @ts-expect-error Persona agents are always interactive. + workflow('invalid-persona-interactive').agent('integrations', { + persona: 'nango-integrations', + interactive: false, + }) + ).toThrow('Persona agent "integrations" cannot define'); + }); + + it('rejects ambiguous untyped agent definitions at runtime', () => { + expect(() => + workflow('invalid-both').agent('integrations', { + cli: 'codex', + persona: 'nango-integrations', + } as never) + ).toThrow('Agent "integrations" must define exactly one of "cli" or "persona"'); + expect(() => workflow('invalid-neither').agent('integrations', {} as never)).toThrow( + 'Agent "integrations" must define exactly one of "cli" or "persona"' + ); }); it('rejects mutually exclusive cwd and workdir options', () => { diff --git a/packages/core/src/builder.ts b/packages/core/src/builder.ts index 948a5c3..51d7161 100644 --- a/packages/core/src/builder.ts +++ b/packages/core/src/builder.ts @@ -333,17 +333,64 @@ export class WorkflowBuilder { /** Add an agent definition. */ agent(name: string, options: AgentOptions): this { - const def = { - name, - ...(options.cli ? { cli: options.cli } : {}), - ...(options.persona ? { persona: options.persona } : {}), - } as AgentDefinition; + const runtimeOptions = options as AgentOptionsBase & { + cli?: AgentCli; + persona?: string; + }; + const hasCli = runtimeOptions.cli !== undefined; + const hasPersona = runtimeOptions.persona !== undefined; + if (hasCli === hasPersona) { + throw new Error(`Agent "${name}" must define exactly one of "cli" or "persona"`); + } + + const sharedConstraints: Omit = {}; + if (runtimeOptions.maxTokens !== undefined) + sharedConstraints.maxTokens = runtimeOptions.maxTokens; + if (runtimeOptions.timeoutMs !== undefined) + sharedConstraints.timeoutMs = runtimeOptions.timeoutMs; + if (runtimeOptions.retries !== undefined) sharedConstraints.retries = runtimeOptions.retries; + if (runtimeOptions.idleThresholdSecs !== undefined) + sharedConstraints.idleThresholdSecs = runtimeOptions.idleThresholdSecs; + + let def: AgentDefinition; + if (runtimeOptions.persona !== undefined) { + if ( + runtimeOptions.role !== undefined || + runtimeOptions.model !== undefined || + runtimeOptions.preset !== undefined || + runtimeOptions.interactive === false + ) { + throw new Error( + `Persona agent "${name}" cannot define "role", "model", "preset", or "interactive: false"` + ); + } + def = { + name, + persona: runtimeOptions.persona, + ...(runtimeOptions.interactive === true ? { interactive: true } : {}), + ...(Object.keys(sharedConstraints).length > 0 + ? { constraints: sharedConstraints } + : {}), + }; + } else { + const constraints: AgentConstraints = { + ...sharedConstraints, + ...(runtimeOptions.model !== undefined ? { model: runtimeOptions.model } : {}), + }; + def = { + name, + cli: runtimeOptions.cli!, + ...(runtimeOptions.role !== undefined ? { role: runtimeOptions.role } : {}), + ...(runtimeOptions.preset !== undefined ? { preset: runtimeOptions.preset } : {}), + ...(runtimeOptions.interactive !== undefined + ? { interactive: runtimeOptions.interactive } + : {}), + ...(Object.keys(constraints).length > 0 ? { constraints } : {}), + }; + } - if (options.role !== undefined) def.role = options.role; if (options.task !== undefined) def.task = options.task; if (options.channels !== undefined) def.channels = options.channels; - if (options.preset !== undefined) def.preset = options.preset; - if (options.interactive !== undefined) def.interactive = options.interactive; if (options.skills !== undefined) def.skills = options.skills; if (options.permissions !== undefined) def.permissions = options.permissions; if (options.cwd !== undefined && options.workdir !== undefined) { @@ -356,23 +403,6 @@ export class WorkflowBuilder { if (options.watch !== undefined) def.watch = options.watch; if (options.subscriptions !== undefined) def.subscriptions = options.subscriptions; - if ( - options.model !== undefined || - options.maxTokens !== undefined || - options.timeoutMs !== undefined || - options.retries !== undefined || - options.idleThresholdSecs !== undefined - ) { - const constraints: AgentConstraints = {}; - if (options.model !== undefined) constraints.model = options.model; - if (options.maxTokens !== undefined) constraints.maxTokens = options.maxTokens; - if (options.timeoutMs !== undefined) constraints.timeoutMs = options.timeoutMs; - if (options.retries !== undefined) constraints.retries = options.retries; - if (options.idleThresholdSecs !== undefined) - constraints.idleThresholdSecs = options.idleThresholdSecs; - def.constraints = constraints; - } - this._agents.push(def); return this; }