-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(core): add Goal v3 state protocol #7517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
13a7f74
e012045
953c848
333276c
a171c49
82e2f36
ccc68ee
de4cd2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Qwen Team | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { describe, expect, it } from 'vitest'; | ||
| import type { | ||
| GoalRecord, | ||
| GoalStateCause, | ||
| GoalStateRecordPayloadV2, | ||
| } from './goal-protocol.js'; | ||
| import { projectGoalStateToLegacy } from './goal-legacy-projection.js'; | ||
|
|
||
| const GOAL: GoalRecord = { | ||
| goalId: 'goal-1', | ||
| revision: 2, | ||
| objective: 'ship it', | ||
| status: 'active', | ||
| evidenceCursor: { recordId: 'state-1' }, | ||
| turnCount: 4, | ||
| activeTimeMs: 2000, | ||
| createdAt: 100, | ||
| updatedAt: 200, | ||
| lastReason: 'continuing', | ||
| }; | ||
|
|
||
| function payload( | ||
| cause: GoalStateCause, | ||
| status: GoalRecord['status'] = 'active', | ||
| goal: GoalRecord | null = { ...GOAL, status }, | ||
| ): GoalStateRecordPayloadV2 { | ||
| return { | ||
| v: 2, | ||
| cause, | ||
| snapshot: { v: 2, activity: 'idle', goal }, | ||
| }; | ||
| } | ||
|
|
||
| describe('projectGoalStateToLegacy', () => { | ||
| it.each(['create', 'replace', 'edit', 'resume', 'migrated'] as const)( | ||
| 'projects %s as legacy set with an active projection', | ||
| (cause) => { | ||
| const projected = projectGoalStateToLegacy(payload(cause)); | ||
|
|
||
| expect(projected.goalStatus.kind).toBe('set'); | ||
| expect(projected.activeGoal).toMatchObject({ | ||
| condition: 'ship it', | ||
| iterations: 4, | ||
| setAt: 100, | ||
| lastReason: 'continuing', | ||
| }); | ||
| expect(projected.goalTerminal).toBeNull(); | ||
| }, | ||
| ); | ||
|
|
||
| it('projects completion as achieved and stops active_goal', () => { | ||
| const projected = projectGoalStateToLegacy(payload('complete', 'complete')); | ||
|
|
||
| expect(projected.goalStatus.kind).toBe('achieved'); | ||
| expect(projected.activeGoal).toBeNull(); | ||
| expect(projected.goalTerminal).toMatchObject({ | ||
| kind: 'achieved', | ||
| condition: 'ship it', | ||
| iterations: 4, | ||
| durationMs: 2000, | ||
| }); | ||
| }); | ||
|
|
||
| it('projects clear as cleared using the prior goal objective', () => { | ||
| const projected = projectGoalStateToLegacy( | ||
| payload('clear', 'active', null), | ||
| GOAL, | ||
| ); | ||
|
|
||
| expect(projected.goalStatus).toMatchObject({ | ||
| kind: 'cleared', | ||
| condition: 'ship it', | ||
| }); | ||
| expect(projected.activeGoal).toBeNull(); | ||
| expect(projected.goalTerminal).toBeNull(); | ||
| }); | ||
|
|
||
| it('projects pause as a non-terminal legacy paused state', () => { | ||
| const projected = projectGoalStateToLegacy(payload('pause', 'paused')); | ||
|
|
||
| expect(projected.goalStatus.kind).toBe('paused'); | ||
| expect(projected.activeGoal).toBeNull(); | ||
| expect(projected.goalTerminal).toBeNull(); | ||
| }); | ||
|
|
||
| it.each(['blocked', 'usage_limited'] as const)( | ||
| 'projects %s as a legacy stopped state', | ||
| (status) => { | ||
| const projected = projectGoalStateToLegacy(payload(status, status)); | ||
|
|
||
| expect(projected.goalStatus.kind).toBe('aborted'); | ||
| expect(projected.activeGoal).toBeNull(); | ||
| expect(projected.goalTerminal).toMatchObject({ | ||
| kind: 'aborted', | ||
| condition: 'ship it', | ||
| }); | ||
| }, | ||
| ); | ||
|
|
||
| it('uses checking for active runtime progress without widening the union', () => { | ||
| const projected = projectGoalStateToLegacy( | ||
| payload('turn_finished', 'active'), | ||
| ); | ||
|
|
||
| expect(projected.goalStatus.kind).toBe('checking'); | ||
| expect(projected.activeGoal).not.toBeNull(); | ||
| expect(projected.goalTerminal).toBeNull(); | ||
| }); | ||
|
|
||
| it('does not repeat an aborted terminal after a paused turn finishes', () => { | ||
| const projected = projectGoalStateToLegacy( | ||
| payload('turn_finished', 'paused'), | ||
| ); | ||
|
|
||
| expect(projected.goalStatus.kind).toBe('checking'); | ||
| expect(projected.activeGoal).toBeNull(); | ||
| expect(projected.goalTerminal).toBeNull(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Qwen Team | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import type { GoalRecord, GoalStateRecordPayloadV2 } from './goal-protocol.js'; | ||
|
|
||
| export type LegacyGoalStatusKind = | ||
| | 'set' | ||
| | 'achieved' | ||
| | 'cleared' | ||
| | 'failed' | ||
| | 'aborted' | ||
| | 'paused' | ||
| | 'checking'; | ||
|
|
||
| export interface LegacyGoalStatus { | ||
| type: 'goal_status'; | ||
| kind: LegacyGoalStatusKind; | ||
| condition: string; | ||
| iterations?: number; | ||
| setAt?: number; | ||
| durationMs?: number; | ||
| lastReason?: string; | ||
| } | ||
|
|
||
| export interface LegacyActiveGoal { | ||
| readonly condition: string; | ||
| readonly iterations: number; | ||
| readonly setAt: number; | ||
| readonly tokensAtStart?: number; | ||
| readonly hookId?: string; | ||
| readonly lastReason?: string; | ||
| } | ||
|
|
||
| export interface LegacyGoalTerminal { | ||
| kind: 'achieved' | 'failed' | 'aborted'; | ||
| condition: string; | ||
| iterations: number; | ||
| durationMs: number; | ||
| lastReason?: string; | ||
| } | ||
|
|
||
| export interface LegacyGoalProjection { | ||
| activeGoal: LegacyActiveGoal | null; | ||
| goalStatus: LegacyGoalStatus; | ||
| goalTerminal: LegacyGoalTerminal | null; | ||
| } | ||
|
|
||
| export function projectGoalStateToLegacy( | ||
| payload: GoalStateRecordPayloadV2, | ||
| previousGoal: GoalRecord | null = null, | ||
| ): LegacyGoalProjection { | ||
| const snapshotGoal = payload.snapshot.goal; | ||
| const displayGoal = snapshotGoal ?? previousGoal; | ||
| const kind = legacyStatusKind(payload); | ||
| const goalStatus: LegacyGoalStatus = { | ||
| type: 'goal_status', | ||
| kind, | ||
| condition: displayGoal?.objective ?? '', | ||
| ...(displayGoal ? { iterations: displayGoal.turnCount } : {}), | ||
| ...(displayGoal ? { setAt: displayGoal.createdAt } : {}), | ||
| ...(displayGoal ? { durationMs: displayGoal.activeTimeMs } : {}), | ||
| ...(displayGoal?.lastReason === undefined | ||
| ? {} | ||
| : { lastReason: displayGoal.lastReason }), | ||
|
Comment on lines
+65
to
+67
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The Suggested test addition: it('omits lastReason from projection when absent', () => {
const goalWithoutReason = { ...GOAL, lastReason: undefined };
const projected = projectGoalStateToLegacy(
{ ...SNAPSHOT, goal: goalWithoutReason },
undefined,
{ cause: 'edit', now: 200 },
);
expect(projected.goalStatus).not.toHaveProperty('lastReason');
expect(projected.activeGoal).not.toHaveProperty('lastReason');
});— qwen3.7-max via Qwen Code /review |
||
| }; | ||
| const terminalKind = | ||
| kind === 'achieved' || kind === 'failed' || kind === 'aborted' | ||
| ? kind | ||
| : undefined; | ||
|
|
||
| return { | ||
| activeGoal: | ||
| snapshotGoal?.status === 'active' | ||
| ? { | ||
| condition: snapshotGoal.objective, | ||
| iterations: snapshotGoal.turnCount, | ||
| setAt: snapshotGoal.createdAt, | ||
| ...(snapshotGoal.lastReason === undefined | ||
| ? {} | ||
| : { lastReason: snapshotGoal.lastReason }), | ||
| } | ||
| : null, | ||
| goalStatus, | ||
| goalTerminal: | ||
| terminalKind && displayGoal | ||
| ? { | ||
| kind: terminalKind, | ||
| condition: displayGoal.objective, | ||
| iterations: displayGoal.turnCount, | ||
| durationMs: displayGoal.activeTimeMs, | ||
| ...(displayGoal.lastReason === undefined | ||
| ? {} | ||
| : { lastReason: displayGoal.lastReason }), | ||
| } | ||
| : null, | ||
| }; | ||
| } | ||
|
|
||
| function legacyStatusKind( | ||
| payload: GoalStateRecordPayloadV2, | ||
| ): LegacyGoalStatusKind { | ||
| switch (payload.cause) { | ||
| case 'create': | ||
| case 'replace': | ||
| case 'edit': | ||
| case 'resume': | ||
| case 'migrated': | ||
| return 'set'; | ||
| case 'complete': | ||
| return 'achieved'; | ||
| case 'clear': | ||
| return 'cleared'; | ||
| case 'pause': | ||
| return 'paused'; | ||
| case 'blocked': | ||
| case 'usage_limited': | ||
| return 'aborted'; | ||
| case 'turn_finished': | ||
| case 'verifier_accept': | ||
| case 'verifier_reject': | ||
|
Comment on lines
+122
to
+123
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Add — qwen3.7-max via Qwen Code /review |
||
| return payload.snapshot.goal?.status === 'complete' | ||
| ? 'achieved' | ||
| : payload.snapshot.goal?.status === 'blocked' || | ||
| payload.snapshot.goal?.status === 'usage_limited' | ||
| ? 'aborted' | ||
| : 'checking'; | ||
|
Comment on lines
+121
to
+129
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The // Add parameterized tests mirroring the existing turn_finished patterns:
// payload('turn_finished', 'complete') → goalStatus.kind === 'achieved'
// payload('turn_finished', 'blocked') → goalStatus.kind === 'aborted'
// payload('verifier_accept', 'complete') → goalStatus.kind === 'achieved'
// payload('verifier_reject', 'active') → goalStatus.kind === 'checking'— qwen3.7-max via Qwen Code /review |
||
| default: | ||
| return assertNever(payload.cause); | ||
| } | ||
| } | ||
|
|
||
| function assertNever(value: never): never { | ||
| throw new Error(`Unsupported Goal state cause: ${String(value)}`); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion]
verifier_acceptandverifier_rejectare distinctGoalStateCausevalues handled bylegacyStatusKindbut are never exercised by any test — Failure scenario: a future refactor splits theturn_finished/verifier_accept/verifier_rejectfall-through to give either verifier cause its own branch, and accidentally maps one to the wrongLegacyGoalStatusKind(e.g.,'set'instead of'checking'). The existing parameterized tests only coverturn_finishedand the five "set" causes, so the regression ships undetected.Add
verifier_acceptandverifier_rejectto the parameterized test, e.g. as separate cases that verifylegacyStatusKindmapping produces the correctLegacyGoalStatusKindfor each verifier cause × goal-status combination.— qwen3.7-max via Qwen Code /review