diff --git a/.changeset/pause-goal-clock-on-close.md b/.changeset/pause-goal-clock-on-close.md new file mode 100644 index 00000000000..a687aab79b1 --- /dev/null +++ b/.changeset/pause-goal-clock-on-close.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Exclude time spent with the session closed from goal time budgets. diff --git a/.changeset/remove-goal-time-cap.md b/.changeset/remove-goal-time-cap.md new file mode 100644 index 00000000000..57920efbf9b --- /dev/null +++ b/.changeset/remove-goal-time-cap.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Remove the 24-hour limit on goal time budgets. diff --git a/docs/en/guides/goals.md b/docs/en/guides/goals.md index f54c531bb3c..070495ad5ac 100644 --- a/docs/en/guides/goals.md +++ b/docs/en/guides/goals.md @@ -106,6 +106,8 @@ A goal can stop in three ways: Write stop conditions into the objective. `/goal` does not have a separate stop-limit flag. +Time budgets count only while the goal is active and its session is open. Closing the session saves the elapsed time and pauses the goal. After reopening the session, use `/goal resume` to continue with the remaining budget; time spent closed or paused does not count. + ## Manage goals in the web UI The web UI shows the current goal in a strip below the conversation. Select the strip to expand or collapse its details. When a token budget is configured, the header shows its progress; goals without a token budget do not show a progress bar. diff --git a/docs/zh/guides/goals.md b/docs/zh/guides/goals.md index 8600e266f1f..e1107d73ded 100644 --- a/docs/zh/guides/goals.md +++ b/docs/zh/guides/goals.md @@ -106,6 +106,8 @@ Kimi Code 会保存该目标,把它作为下一条用户消息发送,并进 停止条件需要写在目标本身里。`/goal` 没有单独用于描述停止限制的语法。 +时间预算只在目标处于活跃状态且会话保持打开时计时。关闭会话会保存累计用时并暂停目标。重新打开会话后,使用 `/goal resume` 按剩余预算继续;会话关闭或目标暂停期间不计时。 + ## 在 Web 界面中管理目标 Web 界面会在对话下方显示当前目标条。点击目标条可以展开或收起详细信息。配置 token 预算时,标题栏会显示预算进度;没有配置 token 预算的目标不会显示进度条。 diff --git a/packages/agent-core-v2/src/features/goal/goalService.ts b/packages/agent-core-v2/src/features/goal/goalService.ts index ee03243f271..32a3235d0c6 100644 --- a/packages/agent-core-v2/src/features/goal/goalService.ts +++ b/packages/agent-core-v2/src/features/goal/goalService.ts @@ -6,6 +6,7 @@ import { createDecorator, IInstantiationService } from '#/_base/di/instantiation import { MutableDisposable, type IDisposable } from '#/_base/di/lifecycle'; import { abortError } from '#/_base/utils/abort'; import { isPlainRecord } from '#/_base/utils/canonical-args'; +import type { AgentContext } from '#/agent/agentContext/agentContext'; import { IAgentReminderService } from '#/features/reminder/reminderService'; import { AgentActorService, @@ -47,7 +48,7 @@ import { toKimiErrorPayload, type KimiErrorPayload, } from '#/errors'; -import { MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle'; +import { IAgentLifecycleService, MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle'; import { ISessionUsageService } from '#/session/usage/sessionUsage'; import { IEventDispatcher } from '#/state/eventDispatcher'; import type { ExecutableToolResult } from '#/tool/toolContract'; @@ -886,9 +887,6 @@ function settleWallClock(context: GoalOperationContext, state: GoalState): numbe Math.max(0, context.runtime.get(IGoalDeadlineScheduler).now() - context.effects.liveWallClockStartedAt) ); } - if (state.status === 'active' && state.wallClockResumedAt !== undefined) { - return state.wallClockMs + Math.max(0, Date.now() - state.wallClockResumedAt); - } return state.wallClockMs; } @@ -899,9 +897,6 @@ function liveWallClockMs(context: GoalOperationContext, state: GoalState): numbe Math.max(0, context.runtime.get(IGoalDeadlineScheduler).now() - context.effects.liveWallClockStartedAt) ); } - if (state.status === 'active' && state.wallClockResumedAt !== undefined) { - return state.wallClockMs + Math.max(0, Date.now() - state.wallClockResumedAt); - } return state.wallClockMs; } @@ -950,7 +945,7 @@ function wallClockDeadlineDelay(context: GoalOperationContext): number | undefin budgetMs === undefined || context.effects.liveWallClockStartedAt === undefined ) return undefined; - return Math.max(0, budgetMs - liveWallClockMs(context, state)); + return Math.min(2_147_483_647, Math.max(0, budgetMs - liveWallClockMs(context, state))); } function handleWallClockDeadline(context: GoalOperationContext): void { @@ -1112,6 +1107,12 @@ function createGoalEffectHandlers(runtime: AgentActorContext) isWaitForEnabled: () => isWaitForAvailable(context), }, normalize: () => { normalizeAfterReplay(context); }, + closing: (agent: AgentContext) => { + if (agent !== runtime.agent) return; + const state = runtime.getState().goal; + if (state === null || state.status !== 'active') return; + applyLifecycle(context, state, 'paused', 'Paused after agent closed', 'runtime'); + }, turnStarted: (event: TurnStarted) => { handleTurnLaunched(context, event.turnId, event.origin); }, usageRecorded: (usage: UsageRecordedContext) => { if (usage.agent === runtime.agent) handleUsageRecorded(context, usage); @@ -1187,6 +1188,7 @@ const goalEffects = fromCallback(({ }); const disposables: IDisposable[] = [deadline]; if (input.runtime.agent.agentId === MAIN_AGENT_ID) { + disposables.push(input.runtime.get(IAgentLifecycleService).onWillClose(handlers.closing)); disposables.push(new GoalInjection(handlers.injection, reminderOf(input.runtime))); disposables.push(input.runtime.get(IEventBus).subscribe(TurnStarted, handlers.turnStarted)); disposables.push(input.runtime.get(ISessionUsageService).onDidRecord(handlers.usageRecorded)); diff --git a/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/set-goal-budget.md b/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/set-goal-budget.md index b20ee5baee9..522d305c228 100644 --- a/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/set-goal-budget.md +++ b/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/set-goal-budget.md @@ -12,9 +12,9 @@ Do not invent limits. Do not call this for vague wording such as "spend some tim If the user gives a compound time, convert it to one supported unit before calling this tool. For example, "2 hours and 3 minutes" can be set as `value: 123, unit: "minutes"`. -A time budget must be between 1 second and 24 hours — the tool rejects anything shorter or -longer, telling the user it is not a reasonable goal budget. Turn and token budgets are not -bounded this way; they must be positive and are rounded to the nearest whole number (minimum 1). +A time budget must be at least 1 second and convert to a finite number of milliseconds. +There is no upper duration limit. Turn and token budgets must be positive and are rounded +to the nearest whole number (minimum 1). Supported units: diff --git a/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/setGoalBudgetTool.ts b/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/setGoalBudgetTool.ts index 1e15fc2ea5b..c26a3d31560 100644 --- a/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/setGoalBudgetTool.ts +++ b/packages/agent-core-v2/src/features/goal/tools/set-goal-budget/setGoalBudgetTool.ts @@ -14,7 +14,6 @@ import { } from './set-goal-budget'; const MIN_REASONABLE_TIME_BUDGET_MS = 1_000; -const MAX_REASONABLE_TIME_BUDGET_MS = 24 * 60 * 60 * 1000; export class SetGoalBudgetTool implements ISetGoalBudgetTool { declare readonly _serviceBrand: undefined; @@ -118,7 +117,7 @@ function budgetLimitsFromInput(input: SetGoalBudgetToolInput): GoalBudgetLimits const wallClockBudgetMs = Math.round(toMilliseconds(input.value, input.unit)); if ( wallClockBudgetMs < MIN_REASONABLE_TIME_BUDGET_MS || - wallClockBudgetMs > MAX_REASONABLE_TIME_BUDGET_MS + !Number.isFinite(wallClockBudgetMs) ) { return null; } diff --git a/packages/agent-core-v2/test/agent/loop/loop.test.ts b/packages/agent-core-v2/test/agent/loop/loop.test.ts index 3a10f5c824e..91a23e8fb94 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -142,8 +142,8 @@ describe('Agent loop', () => { [emit] turn.step.started { "time": "