WWDC26 samples give the model a switch-mode tool. Keep that idea, then add a gate. The tool proposes to: Mode. Your table says whether the current mode allows it.
ToolPropose next mode
TableLegal?
StateAccept or reject
let legal: [Mode: Set<Mode>] = [
.gather: [.decide],
.decide: [.act, .gather],
.act: [.review],
.review: [.done, .gather]
]
struct AdvanceModeTool: Tool {
let state: AgentState
let description = "Request a move to a new named mode."
@Generable
struct Arguments {
let to: String
}
func call(arguments: Arguments) async throws -> String {
guard let next = Mode(rawValue: arguments.to) else {
return "Unknown mode."
}
let allowed = legal[state.mode, default: []]
guard allowed.contains(next) else {
return "Blocked: \(state.mode.rawValue) cannot go to \(next.rawValue)."
}
state.mode = next
return "Now \(next.rawValue)."
}
}
Attach this tool only in states that may change mode. Act state should expose the real work tool (save a note, start a reminder), not a free-form toolbox. Search stays in gather, using the retrieval stack or SpotlightSearchTool.
If you mark tool calling as required, give an exit. Session 242 warns that required tool loops need a stop. A FinishReviewTool that moves to done is that stop.
Reject in code. A prompt that says "please do not skip review" is not a gate.
Next, put the table and the session in one loop.
Key concepts
- A mode-change tool proposes a next
Mode. The orchestrator checks the legal table. - Reject illegal transitions in code, not in the prompt.
- Attach the mode-change tool only in states that may change mode.
- A finish tool that moves to
donestops required tool loops.
Takeaways
- Act state exposes the real work tool, not a free-form toolbox.
- Search stays in gather via retrieval or
SpotlightSearchTool. - The table is the contract. The model only proposes.