Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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: 83 keys)
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 84 keys)
// App
// Workspace
// workspaceDirs.ephemeralDirs src/workspace/workspaceDirs/workspaceDirsService.ts
Expand Down Expand Up @@ -117,6 +117,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 @@ -1381,6 +1382,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 @@ -1432,6 +1434,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 @@ -1468,6 +1471,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
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
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/agent/loop/turnOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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 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