Add force_agent_assignment flag to assign_agents for soft-pin semantics
- Dominant language
- Python
- Stars
- 670
- Forks
- 183
- Avg merge
- 17h 7m
- Merged PRs (30d)
- 358
Description
# Plan: `force_agent_assignment` flag for `assign_agents`
Tracks: issue #11174.
## Context — why this revision
The original issue proposed a new field `preferred_agents` (soft-pin) sitting next to
`assign_agents` (hard-pin). In review, the reviewers asked to fold this into the existing
`assign_agents` option by adding a companion boolean flag `force_agent_assignment`
instead of introducing a new list field, with the rationale that the single-list + flag
shape is friendlier to callers.
That design is what this plan specifies. The separate device-capability /
driver-version matching workstream raised in the same review (tracked under the
capability-matching proposal series) is **out of scope here** but noted in § Out of
scope.
## Semantic model
One list (`assign_agents`) + one boolean knob (`force_agent_assignment`) fully express
hard-pin and soft-pin behaviour:
| `assign_agents` | `force_agent_assignment` | Selector behaviour |
|---|---|---|
| empty / unset | (ignored) | Current strategy selection. |
| non-empty | `true` | **Hard pin** — if no listed agent is compatible, raise `NoAvailableAgentError`. *(Current behaviour.)* |
| non-empty | `false` | **Soft pin** — try listed agents first; if none compatible, fall through to strategy selection. Emit a structured log event. |
List order is preference order (matches the existing hard-pin iteration), so clients can
encode ranking inside the list itself. No scoring among preferred agents.
### Default value — decision point
The flag default changes user-visible behaviour and is the main decision this plan asks
reviewers to sign off on:
- **Option A — default `true` (backward-compatible).** Existing callers that pass
`assign_agents` keep their hard-pin semantics. Soft-pin is strictly opt-in via
`force_agent_assignment=false`. Safe, but users hit the "stuck in PENDING" footgun
unless they know the flag exists.
- **Option B — default `false` (friendlier, matches the review comment intent).**
Soft-pin becomes the default, which removes the head-of-line-blocking failure mode for
the common case. Existing callers relying on strict pinning must explicitly set
`force_agent_assignment=true`. **Requires a release note and a minor version bump**;
callers that assumed hard semantics silently get fall-through.
**Recommendation: Option A for the first release, flip to Option B in a subsequent
release** once clients have a chance to adopt the flag. Gate the flip on a deprecation
window announced in release notes. This is the single item that must be confirmed before
PR 1 lands, because it pins the migration default (§ PR 2).
## Wire path to modify
Mapping from the entry points the SDK/REST/GQL expose down to the selector. Each arrow
below gains one new field (`force_agent_assignment: bool`) that travels alongside the
existing agent-list field under whatever name that layer uses.
```
client SDK
ComputeSession.get_or_create(assign_agent=..., force_agent_assignment=...)
client/func/session.py:229
→ params["config"]["agentList"], params["config"]["forceAgentAssignment"]
client/func/session.py:371
↓
REST / GQL wire
CreationConfigV5 / V6 / V7 — agent_list, + new force_agent_assignment
common/dto/manager/session/types.py:335, 387, 452
GraphQL CreateSessionInput — agent_list, + new force_agent_assignment
manager/api/gql/session/types.py:549
↓
REST handler — RBAC gate (hide_agents → superadmin)
manager/api/rest/session/handler.py:382-390
↓
Service action (frozen dataclass)
SessionSchedulingSpec.agent_list, + new .force_agent_assignment
manager/services/session/actions/enqueue_session.py:55-63
↓
Creation spec (repository input)
SessionCreationSpec.designated_agent_list, + new .force_agent_assignment
manager/repositories/scheduler/types/session_creation.py:113
↓
Preparer (enqueue-ready row)
SessionEnqueueData.designated_agent_list, + new .force_agent_assignment
manager/repositories/scheduler/types/session_creation.py:292
set in preparer.py:154
↓
DB — sessions table
sessions.designated_agent_ids (ARRAY[text])
manager/models/session/row.py:698-700
+ NEW sessions.force_agent_assignment (bool NOT NULL DEFAULT )
↓
Scheduler workload
SessionWorkload.designated_agent_ids, + new .force_agent_assignment
manager/sokovan/data/workload.py:107
↓
Selector
_select_agent_tracker_for_requirements(designated_agent_ids, force_agent_assignment)
manager/sokovan/scheduler/provisioner/selectors/selector.py:385-445
```
## Selector change (the behavioural core)
File: `manager/sokovan/scheduler/provisioner/selectors/selector.py`
around lines 430–445.
Current block:
```python
if designated_agent_ids:
for tracker in compatible_trackers:
if tracker.original_agent.agent_id in designated_agent_ids:
return tracker
# raises NoAvailableAgentError with detail
```
Revised block:
```python
if designated_agent_ids:
for tracker in compatible_trackers:
if tracker.original_agent.agent_id in designated_agent_ids:
return tracker
if force_agent_assignment:
# existing detailed error path — hard pin, unchanged
...
raise NoAvailableAgentError(...)
# soft pin: structured log, then fall through to strategy
log.info(
"assign_agents.fall_through(session={}, requested={}, candidates={})",
criteria.session_metadata.session_id,
list(designated_agent_ids),
[t.original_agent.agent_id for t in compatible_trackers],
)
```
Failed-agent deprioritisation (lines 447–475) runs after the soft fall-through just as it
does today.
## RBAC
No new RBAC surface in the first cut. The existing `hide_agents` → superadmin-only gate
at `manager/api/rest/session/handler.py:384-390` already covers any caller supplying an
agent list; the new boolean is tied to the same field and inherits the same gate by
virtue of being rejected at the same point when `agent_list` is rejected.
Out of scope for this PR (noted in issue discussion): a distinct permission for "can use
hard pin" vs "can only use soft pin." Deferrable until there is a concrete request.
## Observability
- **Structured log on soft-pin miss.** `assign_agents.fall_through` with session id,
requested agent ids, and the compatible candidate ids. One line per fall-through,
sampled at info level — enough to diagnose "why did my preference not stick" without
drowning the log on busy pools.
- **No new metric in v1.** If operators start asking "how often are preferences missed?",
a counter can be added in a follow-up; do not speculatively add one.
## PR breakdown
Dependency order: 1 → 2 → 3 → (4 ∥ 5) → 6 → 7.
1. **DB migration.** Add `force_agent_assignment BOOLEAN NOT NULL DEFAULT `
to `sessions`. Backfill is the column default — no data rewrite needed. Follow the
idempotent-migration strategy documented in the alembic README because this lands on
both `main` and the active release branch.
2. **Selector + workload plumbing.** Thread `force_agent_assignment` through
`SessionWorkload` → selector; implement the conditional fall-through above. Unit tests
on the selector covering the six cells of the semantics table.
3. **Enqueue-path plumbing.** `SessionSchedulingSpec` → `SessionCreationSpec` →
`SessionEnqueueData` → preparer → `sessions` row. Persist the flag, round-trip it into
`SessionWorkload` on scheduling.
4. **Wire schemas.** Add `force_agent_assignment` (snake/camel alias
`forceAgentAssignment`) to `CreationConfigV5/V6/V7` and to the GraphQL
`CreateSessionInput`. REST handler passes it through unchanged — no new validation
beyond the existing `hide_agents` gate on `agent_list`.
5. **Client SDK + CLI.** Add `force_agent_assignment: bool = ` kwarg to
`ComputeSession.get_or_create`; surface as `--force-agent-assignment` on the session
create CLI. Gated by the same API-version sentinel as `assign_agent`.
6. **Tests.** Integration test on the full enqueue→schedule path: soft miss falls through
and the session lands on another agent; hard miss still raises. End-to-end test via
the session-create CLI for both modes per the contributor-guide verification rule.
7. **Docs.** Scheduling docs — "Hard vs. soft agent assignment" section with the
semantics table above, the default-value behaviour, and the `hide_agents` RBAC note.
## Out of scope (explicit)
- **Device-capability / driver-version matching.** Belongs to the capability-matching
proposal series raised alongside this review. Some use cases from the original issue
(warm-image, driver-version hints) may be better served by capability matching once
that lands, but soft-pin still has independent value (data-locality, external
orchestration hints) and does not wait on that work.
- **Ranking / scoring among multiple preferred agents.** List order = preference order.
If a real scoring need surfaces later, it is a follow-up.
- **Renaming `assign_agents` / `agent_list` / `designated_agent_ids`** (the review also
surfaced a broader "agent → node" rename idea). Not this PR.
- **New RBAC permission specifically for hard pinning.** Current `hide_agents` gate is
reused as-is.
Contributor guide
Assessment
This issue has not been assessed yet.