-
Notifications
You must be signed in to change notification settings - Fork 2.8k
refactor(onboard): extract agent policy finalization FSM phases #4484
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
Merged
+239
−0
Merged
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
6b753bf
docs(onboard): document FSM migration target
cv fb1b32d
refactor(onboard): centralize machine state metadata
cv c3e4ad6
refactor(onboard): derive session step mapping from FSM metadata
cv 603832c
refactor(onboard): derive progress labels from FSM metadata
cv 4fad8e7
fix(onboard): emit lifecycle events for onboarding start
cv f99e9cb
fix(onboard): emit machine events for resume conflicts
cv 2b60df4
refactor(onboard): introduce explicit state result types
cv 30341b0
refactor(onboard): apply explicit state results through runtime
cv d4ad2d9
refactor(onboard): make finalization return FSM result
cv 356c947
refactor(onboard): make agent setup return FSM result
cv 2296519
refactor(onboard): make policy setup return FSM result
cv 67a9a1e
refactor(onboard): make preflight and gateway return FSM results
cv 46f4a49
refactor(onboard): make sandbox return branch FSM result
cv 9cc15f5
refactor(onboard): return FSM results from provider inference
cv dbbb273
refactor(onboard): add FSM runner shell
cv 6b27a0b
refactor(onboard): consume handler FSM results compatibly
cv 44009ad
refactor(onboard): allow step recording without machine transitions
cv cd6e5f7
refactor(onboard): plumb step mutation options through runtime
cv e266e3b
refactor(onboard): add record-only FSM runner adapter
cv bf4da0b
refactor(onboard): return ordered provider FSM results
cv 212ff4d
refactor(onboard): run live sequence with record-only steps
cv f69f60a
refactor(onboard): let FSM handlers return result sequences
cv 727ac69
refactor(onboard): add sequence runner adapter
cv 75f82f7
refactor(onboard): support FSM runner stop states
cv 59deee6
refactor(onboard): define FSM flow context
cv 25c5abf
refactor(onboard): extract preflight and gateway FSM phases
cv 4d9cc9f
refactor(onboard): extract provider and sandbox FSM phases
cv 8a5b54a
refactor(onboard): extract agent policy finalization FSM phases
cv a1af752
merge(onboard): sync FSM stop states with main
cv 5900708
merge(onboard): sync flow context with stop states
cv 8576e5f
merge(onboard): sync preflight phases with flow context
cv 42eeb90
merge(onboard): sync provider sandbox phases with preflight phases
cv dc8c463
test(onboard): cover sandbox branch metadata passthrough
cv 5c76573
merge(onboard): sync finalization phases with provider sandbox phases
cv 68722ed
test(onboard): run finalization phases through sequence runner
cv 5c7b7ee
Merge branch 'main' into stack/onboard-fsm-flow-context
jyaunches 83c1412
Merge branch 'main' into stack/onboard-fsm-flow-context
cv 6abbbb4
Merge branch 'stack/onboard-fsm-flow-context' into stack/onboard-fsm-…
cv c07f0dd
Merge branch 'stack/onboard-fsm-preflight-gateway-phases' into stack/…
cv cf6c2a9
Merge branch 'stack/onboard-fsm-provider-sandbox-phases' into stack/o…
cv 23e8577
Merge branch 'main' into stack/onboard-fsm-flow-context
cv abbd8c4
chore: apply static formatting for FSM flow stack
cv b592eac
Merge remote-tracking branch 'origin/stack/onboard-fsm-flow-context' …
cv adbbdfa
chore(onboard): format preflight gateway FSM phase
cv 8a6bca2
Merge remote-tracking branch 'origin/stack/onboard-fsm-preflight-gate…
cv baeab37
chore(onboard): format provider sandbox FSM phase
cv 5118bd7
Merge remote-tracking branch 'origin/stack/onboard-fsm-provider-sandb…
cv a64242b
chore(onboard): format finalization FSM phases
cv e55a6d4
Merge branch 'main' into stack/onboard-fsm-agent-policy-finalization-…
cv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
182 changes: 182 additions & 0 deletions
182
src/lib/onboard/machine/flow-phases/agent-policy-finalization.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { | ||
| createSession, | ||
| filterSafeUpdates, | ||
| MACHINE_SNAPSHOT_VERSION, | ||
| normalizeSession, | ||
| sanitizeFailure, | ||
| type Session, | ||
| type SessionUpdates, | ||
| } from "../../../state/onboard-session"; | ||
| import type { OnboardFlowContext } from "../flow-context"; | ||
| import { advanceTo, completeOnboardMachine } from "../result"; | ||
| import { OnboardRuntime, type OnboardRuntimeDeps } from "../runtime"; | ||
| import { runOnboardSequenceWithRunner } from "../sequence-runner"; | ||
| import { | ||
| createAgentSetupPhase, | ||
| createFinalizationPhase, | ||
| createOpenclawSetupPhase, | ||
| createPoliciesPhase, | ||
| createPostVerifyPhase, | ||
| } from "./agent-policy-finalization"; | ||
|
|
||
| function context(): OnboardFlowContext<null, null, null> { | ||
| return { | ||
| resume: false, | ||
| fresh: false, | ||
| session: createSession(), | ||
| agent: null, | ||
| recordedSandboxName: null, | ||
| requestedSandboxName: null, | ||
| sandboxName: "my-assistant", | ||
| fromDockerfile: null, | ||
| model: "model", | ||
| provider: "provider", | ||
| endpointUrl: null, | ||
| credentialEnv: null, | ||
| hermesAuthMethod: null, | ||
| hermesToolGateways: [], | ||
| preferredInferenceApi: null, | ||
| nimContainer: null, | ||
| webSearchConfig: null, | ||
| webSearchSupported: true, | ||
| selectedMessagingChannels: [], | ||
| gpu: null, | ||
| sandboxGpuConfig: null, | ||
| gpuPassthrough: false, | ||
| }; | ||
| } | ||
|
|
||
| function cloneSession(session: Session): Session { | ||
| return normalizeSession(JSON.parse(JSON.stringify(session))) ?? session; | ||
| } | ||
|
|
||
| function createRuntime(initialSession: Session = createSession()) { | ||
| let session = cloneSession(initialSession); | ||
| const updateSession = (mutator: (value: Session) => Session | void): Session => { | ||
| session = cloneSession(mutator(cloneSession(session)) ?? session); | ||
| return cloneSession(session); | ||
| }; | ||
| const deps: OnboardRuntimeDeps = { | ||
| loadSession: () => cloneSession(session), | ||
| createSession, | ||
| saveSession: (next) => { | ||
| session = cloneSession(next); | ||
| return cloneSession(session); | ||
| }, | ||
| updateSession, | ||
| markStepStarted: () => cloneSession(session), | ||
| markStepComplete: (_stepName, updates: SessionUpdates = {}) => | ||
| updateSession((current) => { | ||
| Object.assign(current, filterSafeUpdates(updates)); | ||
| return current; | ||
| }), | ||
| markStepCompleteRecordOnly: (_stepName, updates: SessionUpdates = {}) => | ||
| updateSession((current) => { | ||
| Object.assign(current, filterSafeUpdates(updates)); | ||
| return current; | ||
| }), | ||
| markStepSkipped: () => cloneSession(session), | ||
| markStepFailed: (stepName, message) => | ||
| updateSession((current) => { | ||
| current.status = "failed"; | ||
| current.failure = sanitizeFailure({ step: stepName, message, recordedAt: "now" }); | ||
| return current; | ||
| }), | ||
| markStepFailedRecordOnly: () => cloneSession(session), | ||
| completeSession: (updates: SessionUpdates = {}) => | ||
| updateSession((current) => { | ||
| Object.assign(current, filterSafeUpdates(updates)); | ||
| current.status = "complete"; | ||
| current.resumable = false; | ||
| return current; | ||
| }), | ||
| filterSafeUpdates, | ||
| emitEvent: () => undefined, | ||
| now: () => "2026-05-29T00:00:00.000Z", | ||
| }; | ||
| return new OnboardRuntime(deps); | ||
| } | ||
|
|
||
| describe("agent/policy/finalization phases", () => { | ||
| it("creates branch-specific setup phases", async () => { | ||
| const agentPhase = createAgentSetupPhase(async () => ({ result: advanceTo("policies") })); | ||
| const openclawPhase = createOpenclawSetupPhase(async () => ({ result: advanceTo("policies") })); | ||
|
|
||
| expect(agentPhase.state).toBe("agent_setup"); | ||
| expect(openclawPhase.state).toBe("openclaw"); | ||
| await expect(agentPhase.run(context())).resolves.toMatchObject({ | ||
| result: { next: "policies" }, | ||
| }); | ||
| await expect(openclawPhase.run(context())).resolves.toMatchObject({ | ||
| result: { next: "policies" }, | ||
| }); | ||
| }); | ||
|
|
||
| it("maps policies context updates", async () => { | ||
| const phase = createPoliciesPhase(async () => ({ | ||
| context: { selectedMessagingChannels: ["slack"] }, | ||
| result: advanceTo("finalizing"), | ||
| })); | ||
|
|
||
| const result = await phase.run(context()); | ||
|
|
||
| expect(phase.state).toBe("policies"); | ||
| expect(result.context.selectedMessagingChannels).toEqual(["slack"]); | ||
| expect(result.result).toMatchObject({ next: "finalizing" }); | ||
| }); | ||
|
|
||
| it("creates finalization and post-verify phases", async () => { | ||
| const finalizing = createFinalizationPhase(async () => ({ result: advanceTo("post_verify") })); | ||
| const postVerify = createPostVerifyPhase(async () => ({ | ||
| result: completeOnboardMachine({ sandboxName: "my-assistant" }), | ||
| })); | ||
|
|
||
| expect(finalizing.state).toBe("finalizing"); | ||
| expect(postVerify.state).toBe("post_verify"); | ||
| await expect(finalizing.run(context())).resolves.toMatchObject({ | ||
| result: { next: "post_verify" }, | ||
| }); | ||
| await expect(postVerify.run(context())).resolves.toMatchObject({ | ||
| result: { type: "complete" }, | ||
| }); | ||
| }); | ||
|
|
||
| it("runs branch-to-completion phases through the strict FSM runner", async () => { | ||
| const initialSession = createSession({ | ||
| machine: { | ||
| version: MACHINE_SNAPSHOT_VERSION, | ||
| state: "openclaw", | ||
| stateEnteredAt: "2026-05-29T00:00:00.000Z", | ||
| revision: 0, | ||
| }, | ||
| }); | ||
|
|
||
| const result = await runOnboardSequenceWithRunner({ | ||
| context: context(), | ||
| runtime: createRuntime(initialSession), | ||
| phases: [ | ||
| createOpenclawSetupPhase(async () => ({ result: advanceTo("policies") })), | ||
| createPoliciesPhase(async () => ({ | ||
| context: { selectedMessagingChannels: ["slack"] }, | ||
| result: advanceTo("finalizing"), | ||
| })), | ||
| createFinalizationPhase(async () => ({ result: advanceTo("post_verify") })), | ||
| createPostVerifyPhase(async () => ({ | ||
| result: completeOnboardMachine({ sandboxName: "my-assistant" }), | ||
| })), | ||
| ], | ||
| }); | ||
|
|
||
| expect(result.session).toMatchObject({ | ||
| status: "complete", | ||
| sandboxName: "my-assistant", | ||
| machine: { state: "complete" }, | ||
| }); | ||
| expect(result.context.selectedMessagingChannels).toEqual(["slack"]); | ||
| }); | ||
| }); | ||
57 changes: 57 additions & 0 deletions
57
src/lib/onboard/machine/flow-phases/agent-policy-finalization.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import type { OnboardFlowContext, OnboardFlowPhaseResult } from "../flow-context"; | ||
| import { mergeOnboardFlowContext, onboardFlowPhaseResult } from "../flow-context"; | ||
| import type { OnboardSequencePhase } from "../sequence-runner"; | ||
|
|
||
| type FlowPhaseHandler<Context extends OnboardFlowContext> = (context: Context) => Promise<{ | ||
| context?: Partial<Context>; | ||
| result: OnboardFlowPhaseResult<Context>["result"]; | ||
| }>; | ||
|
|
||
| function createFlowPhase<Context extends OnboardFlowContext>( | ||
| state: OnboardSequencePhase<Context>["state"], | ||
| runPhase: FlowPhaseHandler<Context>, | ||
| ): OnboardSequencePhase<Context> { | ||
| return { | ||
| state, | ||
| async run(context) { | ||
| const result = await runPhase(context); | ||
| return onboardFlowPhaseResult( | ||
| result.context ? mergeOnboardFlowContext(context, result.context) : context, | ||
| result.result, | ||
| ); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export function createAgentSetupPhase<Context extends OnboardFlowContext>( | ||
| runAgentSetup: FlowPhaseHandler<Context>, | ||
| ): OnboardSequencePhase<Context> { | ||
| return createFlowPhase("agent_setup", runAgentSetup); | ||
| } | ||
|
|
||
| export function createOpenclawSetupPhase<Context extends OnboardFlowContext>( | ||
| runOpenclawSetup: FlowPhaseHandler<Context>, | ||
| ): OnboardSequencePhase<Context> { | ||
| return createFlowPhase("openclaw", runOpenclawSetup); | ||
| } | ||
|
|
||
| export function createPoliciesPhase<Context extends OnboardFlowContext>( | ||
| runPolicies: FlowPhaseHandler<Context>, | ||
| ): OnboardSequencePhase<Context> { | ||
| return createFlowPhase("policies", runPolicies); | ||
| } | ||
|
|
||
| export function createFinalizationPhase<Context extends OnboardFlowContext>( | ||
| runFinalization: FlowPhaseHandler<Context>, | ||
| ): OnboardSequencePhase<Context> { | ||
| return createFlowPhase("finalizing", runFinalization); | ||
| } | ||
|
|
||
| export function createPostVerifyPhase<Context extends OnboardFlowContext>( | ||
| runPostVerify: FlowPhaseHandler<Context>, | ||
| ): OnboardSequencePhase<Context> { | ||
| return createFlowPhase("post_verify", runPostVerify); | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Preserve in-place session mutations when mutator returns
void.At Line 61,
mutator(...) ?? sessiondrops mutations if the mutator uses the allowedvoidreturn path. Use the mutated draft as fallback to honor theOnboardRuntimeDeps.updateSessioncontract.Suggested patch
const updateSession = (mutator: (value: Session) => Session | void): Session => { - session = cloneSession(mutator(cloneSession(session)) ?? session); + const draft = cloneSession(session); + session = cloneSession(mutator(draft) ?? draft); return cloneSession(session); };📝 Committable suggestion
🤖 Prompt for AI Agents