Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/unlimited-max-steps-per-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code": minor
---

Remove the default per-turn step limit of 1000. Users can still set `max_steps_per_turn` in config to enforce a custom limit.
3 changes: 1 addition & 2 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ max_context_size = 262144
mode = "auto"

[loop_control]
max_steps_per_turn = 1000
max_retries_per_step = 3
reserved_context_size = 50000

Expand Down Expand Up @@ -154,7 +153,7 @@ max_context_size = 1047576

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `max_steps_per_turn` | `integer` | `1000` | Maximum number of steps per turn |
| `max_steps_per_turn` | `integer` | | Maximum number of steps per turn; set to `0` or leave unset for unlimited. Setting `0` is useful for overriding a previously configured limit. |
| `max_retries_per_step` | `integer` | `3` | Maximum retries per step |
| `reserved_context_size` | `integer` | — | Number of tokens reserved for response generation; compaction is triggered when the context approaches this threshold |

Expand Down
3 changes: 1 addition & 2 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ max_context_size = 262144
mode = "auto"

[loop_control]
max_steps_per_turn = 1000
max_retries_per_step = 3
reserved_context_size = 50000

Expand Down Expand Up @@ -154,7 +153,7 @@ max_context_size = 1047576

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `max_steps_per_turn` | `integer` | `1000` | 单轮最大步数 |
| `max_steps_per_turn` | `integer` | | 单轮最大步数;不设置或设为 `0` 则无上限。设为 `0` 可用于显式覆盖此前已配置的限制。 |
| `max_retries_per_step` | `integer` | `3` | 单步最大重试次数 |
| `reserved_context_size` | `integer` | — | 预留给响应生成的 token 数;上下文逼近该阈值时触发压缩 |

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const PermissionConfigSchema = z.object({
export type PermissionConfig = z.infer<typeof PermissionConfigSchema>;

export const LoopControlSchema = z.object({
maxStepsPerTurn: z.number().int().min(1).optional(),
maxStepsPerTurn: z.number().int().min(0).optional(),
maxRetriesPerStep: z.number().int().min(0).optional(),
maxRalphIterations: z.number().int().min(-1).optional(),
reservedContextSize: z.number().int().min(0).optional(),
Expand Down
6 changes: 2 additions & 4 deletions packages/agent-core/src/loop/run-turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import type {
TurnResult,
} from './types';

const DEFAULT_MAX_STEPS = 1000;

export interface RunTurnInput {
readonly turnId: string;
readonly signal: AbortSignal;
Expand All @@ -53,7 +51,7 @@ export async function runTurn(input: RunTurnInput): Promise<TurnResult> {
tools,
hooks,
log,
maxSteps = DEFAULT_MAX_STEPS,
maxSteps,
maxRetryAttempts,
} = input;
let usage: TokenUsage = emptyUsage();
Expand All @@ -69,7 +67,7 @@ export async function runTurn(input: RunTurnInput): Promise<TurnResult> {
while (true) {
signal.throwIfAborted();

if (steps >= maxSteps) {
if (maxSteps !== undefined && maxSteps > 0 && steps >= maxSteps) {
throw createMaxStepsExceededError(maxSteps);
}

Expand Down
56 changes: 33 additions & 23 deletions packages/agent-core/test/loop/turn-lifecycle.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,37 +166,47 @@ describe('runTurn — turn lifecycle', () => {
expect(interruptedTypes).toContain('max_steps');
});

it('throws immediately when maxSteps=0 (boundary)', async () => {
const { error, sink, llm, context } = await runTurnExpectingThrow({
it('does not enforce a max step limit when maxSteps is 0', async () => {
const echo = new EchoTool();
const { result } = await runTurn({
maxSteps: 0,
responses: [makeEndTurnResponse('never reached')],
tools: [echo],
responses: [
makeToolUseResponse([makeToolCall('echo', { text: '1' }, 'a')]),
makeToolUseResponse([makeToolCall('echo', { text: '2' }, 'b')]),
makeEndTurnResponse('done'),
],
});

expect(error).toBeInstanceOf(KimiError);
expect((error as KimiError).code).toBe(ErrorCodes.LOOP_MAX_STEPS_EXCEEDED);
expect(llm.callCount).toBe(0);
// No step envelope was opened
expect(context.stepBegins().length).toBe(0);
expect(context.stepEnds().length).toBe(0);
expect(sink.count('step.begin')).toBe(0);
// turn.interrupted is still emitted without pretending a step started.
const interrupted = sink.byType('turn.interrupted');
expect(interrupted.length).toBe(1);
expect(interrupted[0]?.reason).toBe('max_steps');
expect(interrupted[0]?.attemptedSteps).toBe(0);
expect(interrupted[0]?.activeStep).toBeUndefined();
expect(result.stopReason).toBe('end_turn');
expect(result.steps).toBe(3);
expect(echo.calls).toEqual([
{ id: 'a', turnId: 'turn-1', args: { text: '1' } },
{ id: 'b', turnId: 'turn-1', args: { text: '2' } },
]);
});

it('uses a generous default maxSteps when omitted', async () => {
// We don't want to actually exhaust the default; just prove that a
// single end_turn step under no explicit limit succeeds. The exact
// numeric default is an implementation detail, but it must allow at
// least one step.
it('does not enforce a max step limit when maxSteps is omitted', async () => {
const echo = new EchoTool();
const { result } = await runTurn({
responses: [makeEndTurnResponse('ok')],
tools: [echo],
responses: [
makeToolUseResponse([makeToolCall('echo', { text: '1' }, 'a')]),
makeToolUseResponse([makeToolCall('echo', { text: '2' }, 'b')]),
makeToolUseResponse([makeToolCall('echo', { text: '3' }, 'c')]),
makeToolUseResponse([makeToolCall('echo', { text: '4' }, 'd')]),
makeEndTurnResponse('done'),
],
});

expect(result.stopReason).toBe('end_turn');
expect(result.steps).toBe(1);
expect(result.steps).toBe(5);
expect(echo.calls).toEqual([
{ id: 'a', turnId: 'turn-1', args: { text: '1' } },
{ id: 'b', turnId: 'turn-1', args: { text: '2' } },
{ id: expect.any(String), turnId: 'turn-1', args: { text: '3' } },
{ id: expect.any(String), turnId: 'turn-1', args: { text: '4' } },
]);
});

it('aggregates usage across steps including cache fields', async () => {
Expand Down
1 change: 0 additions & 1 deletion packages/node-sdk/examples/kimi-harness-config-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ async function main(): Promise<void> {
},
},
loopControl: {
maxStepsPerTurn: 1000,
maxRetriesPerStep: 3,
maxRalphIterations: 0,
reservedContextSize: 50000,
Expand Down
2 changes: 0 additions & 2 deletions packages/node-sdk/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ capabilities = ["image_in", "thinking", "video_in"]
display_name = "Kimi for Coding"

[loop_control]
max_steps_per_turn = 1000
max_retries_per_step = 3
max_ralph_iterations = 0
reserved_context_size = 50000
Expand Down Expand Up @@ -111,7 +110,6 @@ describe('SDK config TOML', () => {
});

expect(config.loopControl).toEqual({
maxStepsPerTurn: 1000,
maxRetriesPerStep: 3,
maxRalphIterations: 0,
reservedContextSize: 50000,
Expand Down
Loading