refactor(agent-loop): unify tool-call scheduling on one rolling pool + factory cap default
Run every ordered group through the same rolling pool: an exclusive call is a pool of one (a barrier), dropping the separate runExclusive path and the redundant post-grouping executionMode re-query. Behavior is unchanged — the parallel-tool-calls snapshot and the full scheduler unit suite (barriers, cap, abort, model-order results) stay green. Add AgentLoop.Config.maxParallelToolCalls as a factory-wide default applied to every agent create/createAgent/resume mints (per-agent option overrides it), forwarded through agent-core so it reaches front doors that expose no cap field of their own. Trim the isConcurrencySafe JSDoc to the local contract and link the parallel-tool-call RFC for the full rationale; document the field on the canonical core-data-structures page.
This commit is contained in:
@@ -344,6 +344,16 @@ export { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from './constants.ts'
|
||||
|
||||
/** Plugin configuration for declarative startup agents. */
|
||||
export interface Config {
|
||||
/**
|
||||
* Default concurrent tool-call cap applied to every agent this factory
|
||||
* creates (declarative startup agents and factory callers such as the ACP,
|
||||
* stdio, and SDK front doors that go through `create`/`createAgent`/`resume`).
|
||||
* A positive integer; a per-agent `maxParallelToolCalls` overrides it, and an
|
||||
* agent with neither falls back to {@link DEFAULT_MAX_PARALLEL_TOOL_CALLS}.
|
||||
* This is the single `cordis.yml` knob that reaches agents whose front door
|
||||
* does not expose its own cap field.
|
||||
*/
|
||||
maxParallelToolCalls?: number
|
||||
/** Agents created or resumed at plugin startup. */
|
||||
agents: (AgentOptions & {
|
||||
/** Registry identity for the live agent. */
|
||||
@@ -366,6 +376,9 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
|
||||
/** Runtime schema for declarative agents. */
|
||||
static Config = z.object({
|
||||
// The factory-wide default cap; a per-agent value overrides it. A positive
|
||||
// integer, validated here so a bad cordis.yml value fails at load.
|
||||
maxParallelToolCalls: z.number().step(1).min(1),
|
||||
agents: z.array(z.object({
|
||||
id: z.string().required(),
|
||||
model: z.string(),
|
||||
@@ -410,6 +423,22 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the factory-wide default cap into one agent's options. A per-agent
|
||||
* `maxParallelToolCalls` wins; otherwise the `Config.maxParallelToolCalls`
|
||||
* default applies, reaching factory callers (ACP/stdio/SDK front doors) whose
|
||||
* own config does not set a cap. Absent both, the loop falls back to
|
||||
* {@link DEFAULT_MAX_PARALLEL_TOOL_CALLS} at schedule time.
|
||||
* @param options - the caller-supplied agent options.
|
||||
* @returns options with the default cap applied when the caller omitted one.
|
||||
*/
|
||||
private withFactoryDefaults(options: AgentOptions): AgentOptions {
|
||||
if (options.maxParallelToolCalls !== undefined || this.config.maxParallelToolCalls === undefined) {
|
||||
return options
|
||||
}
|
||||
return { ...options, maxParallelToolCalls: this.config.maxParallelToolCalls }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an agent on a fresh per-run session, owned by the accessing fiber.
|
||||
* Constructor-driven config calls use the loop fiber itself.
|
||||
@@ -419,13 +448,14 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
* @returns the published running agent.
|
||||
*/
|
||||
create(id: AgentId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): ReactLoopAgent {
|
||||
validateAgentOptions(options)
|
||||
const resolved = this.withFactoryDefaults(options)
|
||||
validateAgentOptions(resolved)
|
||||
const loopCtx = this.runtime.ctx
|
||||
const transaction = new AgentCreationTransaction(loopCtx, this.ctx, this.ownership, id)
|
||||
try {
|
||||
const sessionId = SessionId(`${id}-session-${randomUUID()}`)
|
||||
const session = loopCtx.sessions.prepare(sessionId, { meta })
|
||||
const agent = transaction.prepare(options, session)
|
||||
const agent = transaction.prepare(resolved, session)
|
||||
transaction.publish('startup')
|
||||
return agent
|
||||
} catch (error: unknown) {
|
||||
@@ -443,7 +473,8 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
* @returns the published handle.
|
||||
*/
|
||||
async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle> {
|
||||
validateAgentOptions(options.agentOptions ?? {})
|
||||
const agentOptions = this.withFactoryDefaults(options.agentOptions ?? {})
|
||||
validateAgentOptions(agentOptions)
|
||||
const transaction = new AgentCreationTransaction(
|
||||
this.runtime.ctx,
|
||||
ownerCtx,
|
||||
@@ -456,7 +487,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
...options.seed === undefined ? {} : { seed: options.seed },
|
||||
...options.meta === undefined ? {} : { meta: options.meta },
|
||||
})
|
||||
const agent = transaction.prepare(options.agentOptions ?? {}, session)
|
||||
const agent = transaction.prepare(agentOptions, session)
|
||||
await transaction.waitFor(options.setup?.(agent.ctx))
|
||||
transaction.assertActive()
|
||||
return transaction.publish('startup')
|
||||
@@ -475,7 +506,6 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
* @returns the published handle.
|
||||
*/
|
||||
async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle> {
|
||||
validateAgentOptions(options.agentOptions ?? {})
|
||||
const persistence = this.runtime.ctx.get('sessionPersistence')
|
||||
if (persistence === undefined) {
|
||||
throw new Error('cannot resume: session persistence is not configured (load a dsh-session-persistence backend)')
|
||||
@@ -489,6 +519,8 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
persistence: SessionPersistence,
|
||||
options: ResumeAgentOptions,
|
||||
): Promise<AgentHandle> {
|
||||
const agentOptions = this.withFactoryDefaults(options.agentOptions ?? {})
|
||||
validateAgentOptions(agentOptions)
|
||||
const transaction = new AgentCreationTransaction(
|
||||
this.runtime.ctx,
|
||||
ownerCtx,
|
||||
@@ -508,7 +540,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
...loaded.meta.seedLength === undefined ? {} : { seedLength: loaded.meta.seedLength },
|
||||
},
|
||||
})
|
||||
const agent = transaction.prepare(options.agentOptions ?? {}, session)
|
||||
const agent = transaction.prepare(agentOptions, session)
|
||||
await transaction.waitFor(options.setup?.(agent.ctx))
|
||||
transaction.assertActive()
|
||||
return transaction.publish('resume')
|
||||
|
||||
Reference in New Issue
Block a user