Skip to content

Commit e79b927

Browse files
committed
feat(agent-runtime): subagent step budgets bounded by parent's remaining steps
createAgentState now gives a spawned agent min(MAX_AGENT_STEPS_DEFAULT, parent.stepsRemaining) instead of a flat 100. The effort dial caps the parent via maxStepsForEffort, so the clamp makes it bound the whole swarm: at effort=low (30) children can no longer get a fresh 100-step allowance, and a nearly-depleted parent cannot spawn runaway children. Covers both spawn-agents and spawn-agent-inline (both go through createAgentState).
1 parent 4242ba4 commit e79b927

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { z } from 'zod'
3+
4+
import { MAX_AGENT_STEPS_DEFAULT } from '@levelcode/common/constants/agents'
5+
6+
import { createAgentState } from '../tools/handlers/tool/spawn-agent-utils'
7+
8+
import type { AgentTemplate } from '@levelcode/common/types/agent-template'
9+
import type { AgentState } from '@levelcode/common/types/session-state'
10+
11+
function template(): AgentTemplate {
12+
return {
13+
id: 'test-agent',
14+
displayName: 'Test Agent',
15+
spawnerPrompt: 'Test spawner prompt',
16+
model: 'test-model',
17+
inputSchema: {},
18+
outputMode: 'last_message',
19+
includeMessageHistory: false,
20+
inheritParentSystemPrompt: false,
21+
mcpServers: {},
22+
toolNames: [],
23+
spawnableAgents: [],
24+
systemPrompt: '',
25+
instructionsPrompt: '',
26+
stepPrompt: '',
27+
} as unknown as AgentTemplate
28+
}
29+
30+
function parent(stepsRemaining: number): AgentState {
31+
return {
32+
agentId: 'parent',
33+
agentType: 'main-agent',
34+
agentContext: {},
35+
ancestorRunIds: [],
36+
subagents: [],
37+
childRunIds: [],
38+
messageHistory: [],
39+
stepsRemaining,
40+
creditsUsed: 0,
41+
directCreditsUsed: 0,
42+
output: undefined,
43+
parentId: undefined,
44+
systemPrompt: '',
45+
toolDefinitions: {},
46+
contextTokenCount: 0,
47+
}
48+
}
49+
50+
describe('createAgentState — subagent step budget', () => {
51+
test('child never exceeds the standard budget', () => {
52+
const child = createAgentState('test-agent', template(), parent(400), {})
53+
expect(child.stepsRemaining).toBe(MAX_AGENT_STEPS_DEFAULT)
54+
})
55+
56+
test('child is clamped to a low-budget parent (effort=low swarm bound)', () => {
57+
const child = createAgentState('test-agent', template(), parent(30), {})
58+
expect(child.stepsRemaining).toBe(30)
59+
})
60+
61+
test('a nearly-depleted parent cannot spawn a runaway child', () => {
62+
const child = createAgentState('test-agent', template(), parent(3), {})
63+
expect(child.stepsRemaining).toBe(3)
64+
})
65+
66+
test('missing parent budget falls back to the standard budget', () => {
67+
const state = parent(30)
68+
delete (state as { stepsRemaining?: number }).stepsRemaining
69+
const child = createAgentState('test-agent', template(), state, {})
70+
expect(child.stepsRemaining).toBe(MAX_AGENT_STEPS_DEFAULT)
71+
})
72+
73+
test('schema validation still accepts the spawned state shape', () => {
74+
const child = createAgentState('test-agent', template(), parent(10), {})
75+
expect(z.number().int().positive().safeParse(child.stepsRemaining).success).toBe(
76+
true,
77+
)
78+
})
79+
})

‎packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,15 @@ export function createAgentState(
341341
})
342342
}
343343

344+
// The child can never outlive its parent's budget: the effort dial caps
345+
// the parent (maxStepsForEffort → run.ts), so clamping here bounds the
346+
// whole swarm. A parent at `low` (30) can no longer spawn children with
347+
// a flat 100-step allowance.
348+
const stepsRemaining = Math.min(
349+
MAX_AGENT_STEPS_DEFAULT,
350+
parentAgentState.stepsRemaining ?? MAX_AGENT_STEPS_DEFAULT,
351+
)
352+
344353
return {
345354
agentId,
346355
agentType,
@@ -352,7 +361,7 @@ export function createAgentState(
352361
subagents: [],
353362
childRunIds: [],
354363
messageHistory,
355-
stepsRemaining: MAX_AGENT_STEPS_DEFAULT,
364+
stepsRemaining,
356365
creditsUsed: 0,
357366
directCreditsUsed: 0,
358367
output: undefined,

0 commit comments

Comments
 (0)