Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
29bee63
docs: add goal implementation plans
chengluyu May 29, 2026
040a06c
Phase 1a: add SessionGoalStore durable goal state, session/agent wiri…
chengluyu May 29, 2026
70ee3c6
Phase 1b: add goal.* audit records, audit sink/queue, normalizeMetada…
chengluyu May 29, 2026
c14b025
Phase 2: expose goal lifecycle via SDK and wire the /goal slash comma…
chengluyu May 29, 2026
c5d8a90
Phase 3: add CreateGoal, GetGoal, and UpdateGoal main-agent tools gat…
chengluyu May 29, 2026
687654c
Phase 4a: inject active-goal guidance into the main agent context wit…
chengluyu May 29, 2026
aea58a5
Phase 4b: account goal token usage from every session agent step in T…
chengluyu May 29, 2026
0899188
Phase 4c: add GoalContinuationController for autonomous continuation …
chengluyu May 29, 2026
d0dc822
Phase 4d: add independent GoalEvaluator and make goal completion eval…
chengluyu May 29, 2026
674b2c1
Phase 5: add end-to-end goal session harness, dispatch integration te…
chengluyu May 29, 2026
abb938d
Phase 6: add headless /goal prompt mode with exit codes and summary, …
chengluyu May 29, 2026
a8e7054
Fix: treat goal maxStepsPerTurn as a per-segment continuation checkpo…
chengluyu May 30, 2026
b6b0922
Fix: stop goal turn gracefully when step cap is hit after a budget wr…
chengluyu May 30, 2026
aee3c9c
Fix: inject goal context at continuation boundaries, not per step (ca…
chengluyu May 30, 2026
8047fa2
Fix: active goal completion self-audit prompt and one-time terminal-g…
chengluyu May 30, 2026
5e60773
Phase 7.1: generic slash subcommand autocomplete, wired for /goal
chengluyu May 30, 2026
0f2d5f0
Phase 7.2: drop default turn cap, surface goal counters to the evaluator
chengluyu May 30, 2026
cabe174
Phase 7.4: goal status footer badge and goal.updated event spine
chengluyu May 30, 2026
8bd0e1e
Phase 7.4: record commit hashes in tracker
chengluyu May 30, 2026
2cf71c7
Phase 7.5: render /goal status as a boxed panel like /usage
chengluyu May 30, 2026
3091451
Phase 7.6a: goal.updated change payload and terminal stats on goal.up…
chengluyu May 30, 2026
80db56b
Phase 7.6b: live transcript markers and completion card for goal loop
chengluyu May 30, 2026
a0b046c
Phase 7: defer 6c (resume reconstruction) with decided stats-only-car…
chengluyu May 30, 2026
ac9604c
Pause on interrupt instead of terminal `interrupted`
chengluyu May 30, 2026
b1ce03b
Consolidate lifecycle to active/paused/blocked/complete
chengluyu May 31, 2026
51dbe3d
Deterministic completion message (replaces the live card)
chengluyu May 31, 2026
5a018be
Phase 8: docs + tracker for goal state consolidation
chengluyu May 31, 2026
bc590fa
Fix resume to reset stuck streaks; show blocked in the badge
chengluyu May 31, 2026
57c193f
Make `cancel` the sole discard; rename GoalChange.kind terminal→compl…
chengluyu May 31, 2026
bca52e2
Drop the stale over-budget injection guidance
chengluyu May 31, 2026
27ff684
Paused goals inject nothing; blocked keeps a light note
chengluyu May 31, 2026
4b50e0c
Three /goal UX fixes (autocomplete, hint, evaluator phase)
chengluyu May 31, 2026
f7ee407
Remove the UpdateGoal tool and its model-report plumbing
chengluyu May 31, 2026
c071f87
Rotate the evaluator spinner label from a pool of ten
chengluyu May 31, 2026
6ba1c01
Drop the --max-* budget flags and the "no stop condition" notice
chengluyu May 31, 2026
86aae27
Sequential-turn driver, minimal UpdateGoal, + UI fixes
chengluyu Jun 1, 2026
f447988
Remove dead evaluator spinner; consistent no-goal messages
chengluyu Jun 1, 2026
5fdb513
Document `/goal` command
chengluyu Jun 1, 2026
10ccf44
fix: preserve headless goal completion summary
chengluyu Jun 1, 2026
df3d359
fix: sync goal badge on session resume
chengluyu Jun 1, 2026
a49005e
Warn before starting goals in Manual mode
chengluyu Jun 1, 2026
9c85b9b
Merge origin/main
chengluyu Jun 2, 2026
bf46229
Remove stray agent-code package file
chengluyu Jun 2, 2026
27b05c9
Highlight goal status messages
chengluyu Jun 2, 2026
0f052f9
Keep goal timer running in footer
chengluyu Jun 2, 2026
77e0735
Avoid Anthropic goal completion prefill
chengluyu Jun 2, 2026
38f55a4
Simplify goal completion replay parsing
chengluyu Jun 2, 2026
833e0c3
Fix stale evaluator/continuation-controller references
chengluyu Jun 2, 2026
dd866b6
Restore regex goal completion replay parsing
chengluyu Jun 2, 2026
a9f6271
Remove dead no-progress/failure guard scaffolding from goal mode
chengluyu Jun 2, 2026
d838e15
Remove dead goal evidence plumbing
chengluyu Jun 2, 2026
e68378e
Tighten goal lifecycle guidance
chengluyu Jun 2, 2026
25ae673
Make goal set notice a marker
chengluyu Jun 2, 2026
73adbb2
Space goal status panel
chengluyu Jun 2, 2026
b0815f5
Add goal budget tool
chengluyu Jun 2, 2026
64fd482
Refresh goal changeset
chengluyu Jun 2, 2026
2a9ef5a
Pause goals on rate limits
chengluyu Jun 2, 2026
9100def
Address goal review feedback
chengluyu Jun 2, 2026
4d82148
Merge origin/main
chengluyu Jun 2, 2026
b7f34e1
Fix goal flag test lint
chengluyu Jun 2, 2026
734b1d4
Address more goal review feedback
chengluyu Jun 2, 2026
e7e6879
Mention the goal is experimental
chengluyu Jun 2, 2026
5e68ee6
Track goal lifecycle usage
chengluyu Jun 2, 2026
c0aacc0
Update goal changeset
chengluyu Jun 2, 2026
a22a66c
Refine goal changeset wording
chengluyu Jun 2, 2026
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
15 changes: 15 additions & 0 deletions .changeset/autonomous-goal-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code-sdk": minor
"@moonshot-ai/kimi-code": minor
---

Add experimental goal mode for longer tasks that need more than one turn. Turn it on with `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1` before you start Kimi.

Use `/goal <objective>` in the TUI when you want Kimi to keep working on one task across turns. For example:

```text
/goal Fix the failing checkout test
```

Kimi shows the goal in the TUI and keeps progress visible while it works. Use `/goal status`, `/goal pause`, `/goal resume`, `/goal cancel`, and `/goal replace <objective>` to manage the goal. This feature is still experimental. Try it and tell us what would make it more useful.
102 changes: 102 additions & 0 deletions apps/kimi-code/src/cli/goal-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { GoalSnapshot } from '@moonshot-ai/kimi-code-sdk';

import { parseGoalCommand } from '#/tui/commands/index';

/**
* Headless goal-mode support for the `kimi -p "/goal <objective>"` prompt path.
*
* The goal driver keeps the prompt's turn-run alive across continuation turns
* until the goal reaches a terminal state, so the existing prompt-turn waiter
* already blocks until then. This module adds the create-on-entry parsing, a
* machine-readable summary, and the terminal-status → exit-code mapping.
*/

export interface HeadlessGoalCreate {
readonly objective: string;
readonly replace: boolean;
}

/**
* Exit codes by final goal status. The lifecycle has only one success outcome
* (`complete` → 0) and two resumable stopped states: `blocked` (the system
* stopped pursuing — the model's UpdateGoal, a budget, or an error) and `paused`
* (a turn abort / SIGINT). Both are non-zero — the goal did not complete. An absent goal
* (should not happen on the create path) maps to success.
*/
export const GOAL_EXIT_CODES = {
complete: 0,
blocked: 3,
paused: 6,
} as const;

export function goalExitCode(status: string | undefined): number {
switch (status) {
case 'blocked':
return GOAL_EXIT_CODES.blocked;
case 'paused':
return GOAL_EXIT_CODES.paused;
default:
return GOAL_EXIT_CODES.complete;
}
}

const GOAL_PREFIX = /^\/goal(\s|$)/;

/**
* Parses a headless prompt into a goal-create request, or `undefined` when the
* prompt is not a `/goal` create command (so the caller runs it as a normal
* prompt). Non-create goal subcommands are not supported headless and fall
* through to normal prompt handling.
*/
export function parseHeadlessGoalCreate(
prompt: string,
flagEnabled: boolean,
): HeadlessGoalCreate | undefined {
if (!flagEnabled) return undefined;
const trimmed = prompt.trim();
if (!GOAL_PREFIX.test(trimmed)) return undefined;
const args = trimmed.replace(/^\/goal/, '').trim();
const parsed = parseGoalCommand(args);
if (parsed.kind !== 'create') return undefined;
return { objective: parsed.objective, replace: parsed.replace };
}

export interface GoalSummary {
readonly type: 'goal.summary';
readonly goalId: string | null;
readonly status: string | null;
readonly reason: string | null;
readonly turnsUsed: number | null;
readonly tokensUsed: number | null;
readonly wallClockMs: number | null;
}

export function goalSummaryJson(goal: GoalSnapshot | null): GoalSummary {
if (goal === null) {
return {
type: 'goal.summary',
goalId: null,
status: null,
reason: null,
turnsUsed: null,
tokensUsed: null,
wallClockMs: null,
};
}
return {
type: 'goal.summary',
goalId: goal.goalId,
status: goal.status,
reason: goal.terminalReason ?? null,
turnsUsed: goal.turnsUsed,
tokensUsed: goal.tokensUsed,
wallClockMs: goal.wallClockMs,
};
}

export function formatGoalSummaryText(goal: GoalSnapshot | null): string {
if (goal === null) return 'Goal: no goal found.';
const parts = [`Goal [${goal.status}]`];
if (goal.terminalReason !== undefined) parts.push(goal.terminalReason);
return `${parts.join(': ')} (turns: ${goal.turnsUsed}, tokens: ${goal.tokensUsed})`;
}
95 changes: 83 additions & 12 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
KimiHarness,
log,
type Event,
type GoalSnapshot,
type HookResultEvent,
type Session,
type SessionStatus,
Expand All @@ -19,6 +20,13 @@ import {
import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app';

import type { CLIOptions, PromptOutputFormat } from './options';
import {
formatGoalSummaryText,
goalExitCode,
goalSummaryJson,
parseHeadlessGoalCreate,
type HeadlessGoalCreate,
} from './goal-prompt';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
import { createKimiCodeHostIdentity } from './version';

Expand Down Expand Up @@ -102,16 +110,17 @@ export async function runPrompt(
try {
await harness.ensureConfigFile();
const config = await harness.getConfig();
const { session, resumed, restorePermission, telemetryModel } = await resolvePromptSession(
harness,
opts,
workDir,
config.defaultModel,
stderr,
(restorePermission) => {
restorePromptSessionPermission = restorePermission;
},
);
const { session, resumed, restorePermission, telemetryModel, goalModel } =
await resolvePromptSession(
harness,
opts,
workDir,
config.defaultModel,
stderr,
(restorePermission) => {
restorePromptSessionPermission = restorePermission;
},
);
restorePromptSessionPermission = restorePermission;

initializeCliTelemetry({
Expand All @@ -132,7 +141,17 @@ export async function runPrompt(
});

const outputFormat = opts.outputFormat ?? 'text';
await runPromptTurn(session, opts.prompt!, outputFormat, stdout, stderr);
// Headless goal mode: `kimi -p "/goal <objective>"`. The goal driver keeps
// the turn-run alive across continuation turns, so the normal prompt-turn
// waiter blocks until the goal is terminal; we then emit a summary and set a
// distinct exit code.
const flagMap = await harness.getExperimentalFlags();
const goalCreate = parseHeadlessGoalCreate(opts.prompt!, flagMap['goal-command'] === true);
if (goalCreate !== undefined) {
await runHeadlessGoal(session, goalCreate, goalModel, outputFormat, stdout, stderr);
} else {
await runPromptTurn(session, opts.prompt!, outputFormat, stdout, stderr);
}
writeResumeHint(session.id, outputFormat, stdout, stderr);

withTelemetryContext({ sessionId: session.id }).track('exit', {
Expand All @@ -143,11 +162,55 @@ export async function runPrompt(
}
}

async function runHeadlessGoal(
session: Session,
goal: HeadlessGoalCreate,
model: string | undefined,
outputFormat: PromptOutputFormat,
stdout: PromptOutput,
stderr: PromptOutput,
): Promise<void> {
requireConfiguredModel(model);
await session.createGoal({
objective: goal.objective,
replace: goal.replace,
});
Comment thread
chengluyu marked this conversation as resolved.
let completedSnapshot: GoalSnapshot | null = null;
const unsubscribeGoalEvents = session.onEvent((event) => {
if (
event.type === 'goal.updated' &&
event.change?.kind === 'completion' &&
event.snapshot !== null
) {
completedSnapshot = event.snapshot;
}
});
try {
// The objective is sent as the normal prompt; goal continuation keeps the
// turn alive until a terminal state is reached.
await runPromptTurn(session, goal.objective, outputFormat, stdout, stderr);
Comment thread
chengluyu marked this conversation as resolved.
} finally {
unsubscribeGoalEvents();
const snapshot = completedSnapshot ?? (await session.getGoal()).goal;
if (outputFormat === 'stream-json') {
stdout.write(`${JSON.stringify(goalSummaryJson(snapshot))}\n`);
} else {
stderr.write(`${formatGoalSummaryText(snapshot)}\n`);
}
// Map the terminal goal status to a distinct, non-fatal exit code. A turn
// that threw (error / cancellation) already propagates its own exit path.
if (snapshot !== null && snapshot.status !== 'complete') {
process.exitCode = goalExitCode(snapshot.status);
}
}
}

interface ResolvedPromptSession {
readonly session: Session;
readonly resumed: boolean;
readonly restorePermission: () => Promise<void>;
readonly telemetryModel?: string;
readonly goalModel?: string;
}

async function resolvePromptSession(
Expand Down Expand Up @@ -191,6 +254,7 @@ async function resolvePromptSession(
resumed: true,
restorePermission,
telemetryModel: configuredModel(opts.model, status.model, defaultModel),
goalModel: configuredModel(opts.model, status.model),
};
}

Expand All @@ -214,6 +278,7 @@ async function resolvePromptSession(
resumed: true,
restorePermission,
telemetryModel: configuredModel(opts.model, status.model, defaultModel),
goalModel: configuredModel(opts.model, status.model),
};
}
stderr.write(`No sessions to continue under "${workDir}"; starting a fresh session.\n`);
Expand All @@ -222,7 +287,13 @@ async function resolvePromptSession(
const model = requireConfiguredModel(opts.model, defaultModel);
const session = await harness.createSession({ workDir, model, permission: 'auto' });
installHeadlessHandlers(session);
return { session, resumed: false, restorePermission: async () => {}, telemetryModel: model };
return {
session,
resumed: false,
restorePermission: async () => {},
telemetryModel: model,
goalModel: model,
};
}

async function forcePromptPermission(
Expand Down
41 changes: 41 additions & 0 deletions apps/kimi-code/src/tui/commands/complete-args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { AutocompleteItem } from '@earendil-works/pi-tui';

/**
* A completable token (subcommand or flag) for a slash command's argument
* position. Generic across commands — any `KimiSlashCommand` can build a
* `getArgumentCompletions` from a list of these via {@link completeLeadingArg}.
*/
export interface ArgCompletionSpec {
/** The token inserted on completion, e.g. `pause` or `resume`. */
readonly value: string;
/** Short description shown in the autocomplete menu. */
readonly description: string;
}

/**
* Generic leading-token completer for slash-command arguments.
*
* pi-tui passes `argumentPrefix` = everything typed after `/<command> `. We only
* complete the *first* token: once the user has typed a space after it (moved on
* to an objective, a flag value, etc.) we return `null` so completion never
* clobbers free text. Matching is case-insensitive prefix match on `value`.
*/
export function completeLeadingArg(
specs: readonly ArgCompletionSpec[],
argumentPrefix: string,
): AutocompleteItem[] | null {
if (argumentPrefix.includes(' ')) return null;
const lower = argumentPrefix.toLowerCase();
const items = specs
.filter((spec) => spec.value.toLowerCase().startsWith(lower))
.map((spec) => ({ value: spec.value, label: spec.value, description: spec.description }));
// Nothing left to complete: the user has finished typing a token that is the
// sole remaining match (e.g. `status`). Keeping the menu open here would make
// Enter confirm the no-op completion instead of submitting the command, so we
// suppress it. (A space after the token already returns null above.)
const [only] = items;
if (items.length === 1 && only !== undefined && only.value.toLowerCase() === lower) {
return null;
}
return items.length > 0 ? items : null;
}
6 changes: 6 additions & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
showPermissionPicker,
showSettingsSelector,
} from './config';
import { handleGoalCommand } from './goal';
import { handleProviderCommand } from './provider';
import { handleFeedbackCommand, showMcpServers, showStatusReport, showUsage } from './info';
import { handlePluginsCommand } from './plugins';
Expand Down Expand Up @@ -73,6 +74,7 @@ export {
showUsage,
} from './info';
export { handlePluginsCommand } from './plugins';
export { handleGoalCommand } from './goal';
export {
handleExportDebugZipCommand,
handleExportMdCommand,
Expand Down Expand Up @@ -101,6 +103,7 @@ export interface SlashCommandHost {
track(event: string, props?: Record<string, unknown>): void;
mountEditorReplacement(panel: Component & Focusable): void;
restoreEditor(): void;
restoreInputText(text: string): void;

// Session
requireSession(): Session;
Expand Down Expand Up @@ -270,6 +273,9 @@ async function handleBuiltInSlashCommand(
case 'compact':
await handleCompactCommand(host, args);
return;
case 'goal':
await handleGoalCommand(host, args);
return;
case 'init':
await handleInitCommand(host);
return;
Expand Down
Loading
Loading