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
5 changes: 5 additions & 0 deletions .changeset/drop-subagent-summary-bounce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Subagent final messages are no longer bounced back for expansion when they are under 200 characters.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a changeset for the repeat-breaker behavior

This changeset covers only removal of the short-summary retry, while the same commit also makes the CLI run a new repeat-breaker handoff step and expose subagent stop reasons in Agent and AgentSwarm output. Those are separate user-visible behavior changes, so without a second changeset they will be omitted from the generated release notes.

AGENTS.md reference: AGENTS.md:L85-L87

Useful? React with 👍 / 👎.

6 changes: 5 additions & 1 deletion packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// references become '(circular)', and class instances collapse to a '(ClassName)'
// marker — the wire shape of an entry is the JSON projection of the type here.
//
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 82 keys)
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 83 keys)
// App
// Workspace
// workspaceDirs.ephemeralDirs src/workspace/workspaceDirs/workspaceDirsService.ts
Expand Down Expand Up @@ -116,6 +116,7 @@
// toolDedupe.callKeyByCallId src/agent/toolDedupe/toolDedupeService.ts
// toolDedupe.consecutiveCount src/agent/toolDedupe/toolDedupeService.ts
// toolDedupe.consecutiveKey src/agent/toolDedupe/toolDedupeService.ts
// toolDedupe.handoffPhase src/agent/toolDedupe/toolDedupeService.ts
// toolDedupe.originalCallIndex src/agent/toolDedupe/toolDedupeService.ts
// toolDedupe.stepCalls src/agent/toolDedupe/toolDedupeService.ts
// toolDedupe.syntheticCallIds src/agent/toolDedupe/toolDedupeService.ts
Expand Down Expand Up @@ -1379,6 +1380,7 @@ export interface AgentStateSnapshot {
readonly parentToolCallId?: string;
readonly model?: string;
readonly thinkingEffort?: string;
readonly stopCode?: string;
readonly taskId: string;
readonly description: string;
readonly status: /* AgentTaskStatus — packages/agent-core-v2/src/agent/task/types.ts */ 'completed' | 'failed' | 'running' | 'timed_out' | 'killed' | 'lost';
Expand Down Expand Up @@ -1430,6 +1432,7 @@ export interface AgentStateSnapshot {
readonly parentToolCallId?: string;
readonly model?: string;
readonly thinkingEffort?: string;
readonly stopCode?: string;
readonly taskId: string;
readonly description: string;
readonly status: /* AgentTaskStatus — packages/agent-core-v2/src/agent/task/types.ts */ 'completed' | 'failed' | 'running' | 'timed_out' | 'killed' | 'lost';
Expand Down Expand Up @@ -1466,6 +1469,7 @@ export interface AgentStateSnapshot {
'toolDedupe.callKeyByCallId': Map<string, string>;
'toolDedupe.consecutiveCount': number;
'toolDedupe.consecutiveKey': string | null;
'toolDedupe.handoffPhase': /* HandoffPhase — packages/agent-core-v2/src/agent/toolDedupe/toolDedupeService.ts */ 'idle' | 'active' | 'pending' | 'done';
'toolDedupe.originalCallIndex': Map<string, number>;
'toolDedupe.stepCalls': string[];
'toolDedupe.syntheticCallIds': Set<string>;
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/docs/wire-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ interface TurnEndedPayload {
};
};
durationMs?: number;
stopReason?: string;
}

/**
Expand Down
24 changes: 24 additions & 0 deletions packages/agent-core-v2/src/agent/loop/handoffStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ContinuationStepRequest } from './stepRequest';

export const HANDOFF_STEP_KIND = 'handoff';

export interface HandoffStepObserver {
onMaterialize(): void;
onAbort(): void;
}

export class HandoffStepRequest extends ContinuationStepRequest {
constructor(private readonly observer: HandoffStepObserver) {
super({ kind: HANDOFF_STEP_KIND });
}

override onWillMaterialize(): void {
this.observer.onMaterialize();
}

override abort(): boolean {
const aborted = super.abort();
if (aborted) this.observer.onAbort();
return aborted;
}
}
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/agent/loop/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type LoopRunResult =
readonly type: 'completed';
readonly steps: number;
readonly truncated: boolean;
readonly stopReason?: string;
}
| {
readonly type: 'failed';
Expand Down
59 changes: 44 additions & 15 deletions packages/agent-core-v2/src/agent/loop/loopService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
type TurnSeed,
} from './stepRequest';
import { StepRequestQueue, type StepRequestBatch } from './stepRequestQueue';
import { HANDOFF_STEP_KIND } from './handoffStep';
import {
AssistantDelta,
isDisplayablePromptOrigin,
Expand Down Expand Up @@ -537,6 +538,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
error,
durationMs,
interruptReason,
stopReason: result.type === 'completed' ? result.stopReason : undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop goal continuation after a repeat-breaker handoff

When the main agent has an active goal and the repeat breaker completes its handoff, this emits reason: 'completed' plus stopReason: 'repeat_breaker', but goalService.handleTurnEnded projects only reason and error and therefore immediately calls launchContinuationTurn. The handoff does not actually stop autonomous execution, and because the repeat streak resets in the new turn, a stuck goal can enter another identical 12-call cycle instead of waiting for changed instructions or missing input; the goal continuation path must honor this stop reason.

Useful? React with 👍 / 👎.

}),
);
if (error !== undefined) {
Expand Down Expand Up @@ -675,24 +677,37 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
queue: job?.queue ?? this.standaloneStepQueue,
steps: 0,
lastStopReason: undefined,
forcedStopReason: undefined,
current: undefined,
};
}

private completedResult(runtime: LoopRuntime): LoopRunResult {
const truncated = runtime.lastStopReason === 'truncated';
if (runtime.forcedStopReason === undefined) {
return { type: 'completed', steps: runtime.steps, truncated };
}
return {
type: 'completed',
steps: runtime.steps,
truncated,
stopReason: runtime.forcedStopReason,
};
}

private beginLoopStep(runtime: LoopRuntime): BeginStepResult {
runtime.current = undefined;
runtime.turnSignal.throwIfAborted();
if (!runtime.queue.hasPendingRequests()) {
return {
result: {
type: 'completed',
steps: runtime.steps,
truncated: runtime.lastStopReason === 'truncated',
},
};
return { result: this.completedResult(runtime) };
}
const maxSteps = this.config.get<LoopControl>(LOOP_CONTROL_SECTION)?.maxStepsPerTurn;
if (maxSteps !== undefined && maxSteps > 0 && runtime.steps >= maxSteps) {
if (
maxSteps !== undefined &&
maxSteps > 0 &&
runtime.steps >= maxSteps &&
runtime.queue.peekDriverKind() !== HANDOFF_STEP_KIND

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict the step-cap exemption to the breaker handoff

The new exhausted-cap exemption trusts only the request's public string kind. Because ContinuationStepRequest accepts an arbitrary kind and HandoffStepRequest is publicly exported, any hook can repeatedly enqueue kind: 'handoff' continuations after the configured cap and make max_steps_per_turn stop bounding model requests. Tie this exemption to the single breaker-owned request or a one-shot allowance recorded by the loop rather than a forgeable kind string.

Useful? React with 👍 / 👎.

) {
throw createMaxStepsExceededError(maxSteps);
}
const batch = runtime.queue.takeNextBatch()!;
Expand Down Expand Up @@ -727,14 +742,17 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
}
runtime.current = undefined;
runtime.lastStopReason = result.stopReason;
if (result.stopTurnReason !== undefined && runtime.forcedStopReason === undefined) {
runtime.forcedStopReason = result.stopTurnReason;
}
if (result.stopReason === 'filtered') {
throw new Error2(ErrorCodes.PROVIDER_FILTERED, 'Provider safety policy blocked the response.', {
name: 'ProviderFilteredError',
details: { finishReason: 'filtered' },
});
}
if (!result.hookStopTurn) return undefined;
return { type: 'completed', steps: runtime.steps, truncated: result.stopReason === 'truncated' };
return this.completedResult(runtime);
}

private async handleLoopStepError(
Expand Down Expand Up @@ -861,7 +879,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
}
this.lastRequestTraceId = request.trace.traceId;
this.appendResponseContent(turnId, currentStep, stepUuid, response);
const finishReason = await this.executeStepTools(
const { finishReason, stopTurnReason } = await this.executeStepTools(
turnId,
signal,
currentStep,
Expand All @@ -879,7 +897,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
response.usage,
finishReason,
);
return { stopReason: finishReason, hookStopTurn };
return { stopReason: finishReason, hookStopTurn, stopTurnReason };
} catch (error) {
if (!stepEndAppended) {
this.context.appendLoopEvent({
Expand Down Expand Up @@ -968,13 +986,14 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
stepUuid: string,
response: AgentLLMRequestFinish,
trace: LLMRequestTrace,
): Promise<FinishReason> {
): Promise<StepToolsOutcome> {
let finishReason = response.providerFinishReason ?? 'completed';
if (response.message.toolCalls.length === 0) {
return finishReason === 'tool_calls' ? 'other' : finishReason;
return { finishReason: finishReason === 'tool_calls' ? 'other' : finishReason };
}
const toolCallUuids = new Map<string, string>();
let stopTurn = false;
let stopTurnReason: string | undefined;
for await (const toolResult of this.toolExecutor.execute(response.message.toolCalls, {
signal,
turnId,
Expand Down Expand Up @@ -1003,10 +1022,13 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
toolCallId: toolResult.toolCallId,
result: { output: result.output, isError: result.isError, note: result.note },
});
if (result.stopTurn === true) stopTurn = true;
if (result.stopTurn === true) {
stopTurn = true;
stopTurnReason ??= result.stopTurnReason;
}
}
finishReason = stopTurn ? 'completed' : 'tool_calls';
return finishReason;
return { finishReason, stopTurnReason };
}

private finishStep(
Expand Down Expand Up @@ -1239,6 +1261,7 @@ interface LoopRuntime {
readonly queue: StepRequestQueue;
steps: number;
lastStopReason: FinishReason | undefined;
forcedStopReason: string | undefined;
current: StepRuntime | undefined;
}

Expand Down Expand Up @@ -1277,6 +1300,12 @@ function interruptReasonFor(
type StepExecutionResult = {
readonly stopReason: FinishReason;
readonly hookStopTurn: boolean;
readonly stopTurnReason?: string;
};

type StepToolsOutcome = {
readonly finishReason: FinishReason;
readonly stopTurnReason?: string;
};

type LoopErrorDisposition =
Expand Down
6 changes: 6 additions & 0 deletions packages/agent-core-v2/src/agent/loop/stepRequestQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export class StepRequestQueue {
return this.items.some((item) => !item.aborted);
}

peekDriverKind(): string | undefined {
this.discardAborted();
const driver = this.items.find((item) => !item.mergeable) ?? this.items[0];
return driver?.kind;
}

takeNextBatch(): StepRequestBatch | undefined {
this.discardAborted();
if (this.items.length === 0) return undefined;
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core-v2/src/agent/loop/turnOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const turnEndedSchema = z.object({
reason: z.enum(['completed', 'cancelled', 'failed', 'blocked']),
error: z.custom<KimiErrorPayload>().optional(),
durationMs: z.number().optional(),
stopReason: z.string().optional(),
});

export interface TurnEndedPayload {
Expand All @@ -100,6 +101,7 @@ export interface TurnEndedPayload {
readonly error?: KimiErrorPayload;
readonly durationMs?: number;
readonly interruptReason?: TurnInterruptReason;
readonly stopReason?: string;
Comment thread
RealKai42 marked this conversation as resolved.
}

export class TurnEnded extends AgentEvent2<TurnEndedPayload> {
Expand All @@ -117,6 +119,7 @@ export class TurnEnded extends AgentEvent2<TurnEndedPayload> {
};
if (this.error !== undefined) record['error'] = this.error;
if (this.durationMs !== undefined) record['durationMs'] = this.durationMs;
if (this.stopReason !== undefined) record['stopReason'] = this.stopReason;
record['time'] = this.time;
return record as SerializedEvent2;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core-v2/src/agent/toolDedupe/toolDedupe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface ToolDedupeErrorResult extends ExecutableToolErrorResult {

export type ToolDedupeResult = ToolDedupeSuccessResult | ToolDedupeErrorResult;

export const REPEAT_BREAKER_STOP_REASON = 'repeat_breaker';

export interface IAgentToolDedupeService {
readonly _serviceBrand: undefined;
}
Expand Down
Loading
Loading