diff --git a/api/mcp-tools.json b/api/mcp-tools.json index 60e6945c..d74ccb97 100644 --- a/api/mcp-tools.json +++ b/api/mcp-tools.json @@ -321,7 +321,7 @@ }, { "name": "delegate", - "description": "Create a durable task and scoped attempt. Reuse request_id for lost or uncertain responses. After a confirmed terminal blocked create, use a NEW request_id for a new create; reusing the old id returns the blocked operation. Role and tool guidance prefixes the brief.", + "description": "Create a durable task and scoped attempt. Reuse request_id for lost or uncertain responses. After a confirmed terminal blocked create, use a NEW request_id for a new create; reusing the old id returns the blocked operation. Role and tool guidance prefixes the brief. Choosing model and effort: match the tier to the task's scope, risk and reversibility, not habit. Mechanical or bounded, easily-reversible work takes the cheapest model and lowest effort likely to succeed; design, debugging, security-sensitive review, or anything hard to undo takes the strongest model and highest effort the profile offers. When re-delegating a failed attempt, prefer a higher tier over repeating the same one. Read the resolved provider profile's own models and effort_levels (see the providers list) rather than assuming any provider's names; leaving both unset keeps the profile's defaults.", "input_schema": { "additionalProperties": false, "properties": { @@ -361,10 +361,34 @@ ], "default": null }, + "effort": { + "anyOf": [ + { + "enum": ["minimal", "low", "medium", "high", "xhigh", "max"], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, "mode": { "enum": ["code", "coordination"], "type": "string" }, + "model": { + "anyOf": [ + { + "pattern": "^[A-Za-z0-9._:/-]{1,200}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null + }, "project_id": { "anyOf": [ { diff --git a/api/openapi.yaml b/api/openapi.yaml index 07fe26f4..ff8792e2 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -6636,6 +6636,23 @@ components: pattern: ^[a-z][a-z0-9_-]*$ - type: 'null' title: Provider Profile Id + model: + anyOf: + - type: string + pattern: ^[A-Za-z0-9._:/-]{1,200}$ + - type: 'null' + title: Model + description: >- + One of the resolved provider profile's models. Unset keeps the profile's + default_model. 422 with the allowed models if the profile does not offer it. + effort: + anyOf: + - $ref: '#/components/schemas/EffortLevel' + - type: 'null' + title: Effort + description: >- + One of the resolved provider profile's effort_levels. Unset keeps the profile's + default_effort. 422 with the allowed levels if the profile does not offer it. checkout: anyOf: - $ref: '#/components/schemas/TaskCheckout' @@ -7002,6 +7019,16 @@ components: recovery: type: string description: 'parent_capacity only: how to free capacity.' + allowed_models: + type: array + items: + type: string + description: "model_unavailable only: the resolved profile's offered model ids." + allowed_effort_levels: + type: array + items: + $ref: '#/components/schemas/EffortLevel' + description: "effort_unavailable only: the resolved profile's offered effort levels." required: - reason additionalProperties: true @@ -7975,6 +8002,23 @@ components: pattern: ^[a-z][a-z0-9_-]*$ title: Assigned Profile Id type: string + model: + anyOf: + - type: string + - type: 'null' + default: null + title: Model + description: >- + The model the create request chose, validated against assigned_profile_id. Null + means the profile's default_model. + effort: + anyOf: + - $ref: '#/components/schemas/EffortLevel' + - type: 'null' + default: null + description: >- + The reasoning effort the create request chose, validated against + assigned_profile_id. Null means the profile's default_effort. selection_source: enum: - explicit @@ -8068,6 +8112,8 @@ components: - brief - mode - assigned_profile_id + - model + - effort - selection_source - created_at - updated_at @@ -8153,6 +8199,19 @@ components: configuration_revision: title: Configuration Revision type: string + model: + anyOf: + - type: string + - type: 'null' + default: null + title: Model + description: The model this attempt started with, or null for the profile's default. + effort: + anyOf: + - $ref: '#/components/schemas/EffortLevel' + - type: 'null' + default: null + description: The reasoning effort this attempt started with, or null for the profile's default. agent_ref: $ref: '#/components/schemas/ProviderAgentRef' role: @@ -8321,6 +8380,8 @@ components: - profile_id - native_provider - configuration_revision + - model + - effort - agent_ref - role - depth diff --git a/docs/specs/chat.md b/docs/specs/chat.md index 3cbbaf3f..068b4751 100644 --- a/docs/specs/chat.md +++ b/docs/specs/chat.md @@ -54,6 +54,8 @@ Every session — the main thread and a session's own chat alike, since both are The wire routes are `POST /sessions/{session_id}/model` (`{"model": ""|null}`) and `POST /sessions/{session_id}/effort` (`{"effort": ""|null}`), both `200` with the session's resulting `{session_id, model, effort}`, `404` for a session that is not the owner's, and `422` for an effort level the provider does not offer. They apply equally to the main thread (its `session_id` is `GET /main-thread`'s own) and to any other session. +A task-owned session's override may already be set when the composer first shows it: `TaskCreate`/`delegate`'s optional `model` and `effort` (#318) apply as this same override before the attempt's first brief, not through these routes after the fact — see [delegation](delegation.md#choosing-a-workers-model-and-effort-318). + The A2A message that starts a turn carries the override as metadata, keyed `kagent.dev/model` and `kagent.dev/effort`; `server/internal/kagent` places it on the message `server/internal/sessions` builds for `SendMessage`, without changing its no-replay or `OutcomeUnknown` behavior. For Claude, only an actually-overridden key is sent; a key left at its default is omitted, since absent already means kagent's own harness default and nothing in Claude's native session retains a stale value for an absent key to clear. Codex is the one exception: its harness keeps the last explicit value as a turn/start override for its _later_ turns too, so once a session has had any override (ever, even if later reset), every turn for that session names both keys explicitly from then on — and resetting to "default" sends the profile's own real default model and effort values explicitly, not an empty value, because an empty value does not clear Codex's sticky state, only a concrete one does. A value kagent rejects (an unknown model, most often, since effort is pre-validated) is refused synchronously before the turn starts; this reads as a plain A2A error, shown on the delivery as "send rejected: …", the same clear per-message failure a bad prompt gets — never folded into the generic "could not prepare the kagent session" configuration-failure bucket. ## Sending messages diff --git a/docs/specs/delegation.md b/docs/specs/delegation.md index e3ba58be..540329e5 100644 --- a/docs/specs/delegation.md +++ b/docs/specs/delegation.md @@ -6,10 +6,18 @@ Mainloop's MCP tools create and read durable tasks through the same application Main at depth zero creates supervisors. A supervisor at depth one creates direct children within its persisted project and task tree. Children at depth two and ordinary owner workspace agents cannot delegate. Discovery and direct invocation use the same exact role, depth and grant rules; each task call revalidates persisted identity and current ancestry. Missing, superseded, revoked, archived or terminal attempts cannot authenticate. -`delegate` accepts `request_id`, `title`, `brief`, `mode`, optional `project_id`, `topic_id`, `provider_profile_id` and typed `checkout`. Code tasks require an owned project and checkout; coordination tasks have no repository authority. The service chooses role, depth, owner, root and native configuration. Provider constraints and admission caps apply through the shared service. Reusing a request ID with identical input returns the original operation; changed input conflicts. A returned pending operation is not a running native session. +`delegate` accepts `request_id`, `title`, `brief`, `mode`, optional `project_id`, `topic_id`, `provider_profile_id`, `model`, `effort` and typed `checkout`. Code tasks require an owned project and checkout; coordination tasks have no repository authority. The service chooses role, depth, owner, root and native configuration. Provider constraints and admission caps apply through the shared service. Reusing a request ID with identical input returns the original operation; changed input conflicts. A returned pending operation is not a running native session. Reuse the request ID after a lost or uncertain response. After a confirmed terminal `blocked` create, a new create requires a **new** request ID; replaying the old ID returns the recorded blocked operation. +### Choosing a worker's model and effort (#318) + +`TaskCreate` and `delegate` take optional `model` and `effort`, so the supervisor — the main thread or any agent that calls `delegate` — can fit a worker's cost and capability to the work, rather than always starting it on its provider profile's `default_model` and `default_effort`. Both are validated at create time against the resolved profile (explicit `provider_profile_id`, else the project's or the installation's default): `model` against `ProviderProfile.models`, `effort` against `.effort_levels`. Either named but not offered answers `422` with a `TaskError` whose detail carries `allowed_models` or `allowed_effort_levels` (the profile's own list) alongside `reason` (`model_unavailable` or `effort_unavailable`); the `delegate` tool surfaces the same list in its failure text. Leaving a field unset is unchanged behavior: the attempt starts on the profile's own default, exactly as before this parameter existed. + +A valid choice is recorded on the task and copied onto its attempt when the attempt is made, then applied as the new session's own `/model`/`/effort` override (`SetModelOverride`/`SetEffortOverride`, the same storage `PUT /sessions/{id}/model` and `/effort` use — see [sessions](sessions.md)) before the attempt's first brief is sent, never by a call after the fact that could race the first turn. A resume or a re-delegation that does not name `model`/`effort` keeps the attempt's own values; naming either overrides it for the new attempt. + +The `delegate` tool's description carries short, provider-neutral rules for the choice: weigh the task's scope, risk and reversibility, and whether it is mechanical or needs judgment; default to the cheapest model and lowest effort likely to succeed; prefer a higher tier over repeating the same one when re-delegating a failure. It names no provider's models, pointing instead at the resolved profile's own `models`/`effort_levels` (`GET /providers`, see [provider profiles](providers.md)). Per-project defaults or limits (for example a maximum effort) are not in this slice. + Main's standing context lists its owner's projects with IDs, full repository names and whether an environment is selected. Selection does not guarantee environment readiness. This index is a snapshot when standing context is installed, not a live project query, and is not included for delegated agents. Main's standing context also explains that tasks created directly by the owner in the app or through the owner API are owner-authored and legitimate; main reads them with `task_get`. diff --git a/docs/specs/sessions.md b/docs/specs/sessions.md index 2f1bb890..b87ff8a3 100644 --- a/docs/specs/sessions.md +++ b/docs/specs/sessions.md @@ -12,6 +12,7 @@ History before `e776537` describes the retired Python backend. Where other text - Delivery states in the Go ledger: `queued`, `recorded`, `sending`, `delivered`, `completed`, `failed`, `cancelled`, `uncertain`. Only `recorded` is ever sent: the claimer commits `sending` before the kagent call, holds no database lock or transaction across it, and fences every outcome write on its lease and attempt count, so a claimer that lost its lease changes nothing. `queued` is a message written while another turn was already open; the partial unique index that enforces one open turn per session (`recorded`, `sending`, `delivered`) excludes it, so any number of messages may wait their turn, each in the order it arrived. Whenever a turn ends — settled by the delivery loop, or stopped by the owner — the oldest message still `queued` for that session, if any, is admitted (moved to `recorded`) in the same transaction, one at a time. A session cancelled outright (not just one turn stopped) cancels every message still `queued` behind it instead: there is no next turn for them to wait for. This `queued` is the sessions ledger's own state, distinct from the task report queue's `queued` (`task_event_deliveries.state`, see [tasks](tasks.md#implemented-reports-to-parents-task-event-deliveries-loop)), which now simply admits at once instead of polling `ErrBusy`. - kagent is retried only when it says the message was not accepted, with the same message id and inside the send retry budget (the kagent client does this). Every other doubt makes the delivery `uncertain`, and it is settled by looking the message up (`GetTask`, else `ListTasks`), never by sending again. A `sending` row reclaimed after a crash goes straight to `uncertain` and is looked up. If no task shows the message once the hold has passed, the delivery fails with "not sent again" and the owner may send a new message. - Before a send, the loop makes the binding's kagent Session usable: it creates it on first use (idempotent by request id), resumes a suspended one, and waits for it to be ready (`SESSION_READY_WAIT_SECONDS`, 60 by default). When kagent no longer has the Session (its idle TTL, or deleted out of band) the binding gets a new create request id in a write fenced on the delivery's claim, and a new Session is created (the binding's standing hash is cleared, so the new Session's first message carries the recent conversation again, see the chat spec). A task parked for input or auth is read again at growing intervals up to `SESSION_PARKED_MAX_SECONDS` (300); a failed read backs off the same way. A delivery whose binding is gone fails. The binding records the create request id and the Agent before every create call (`agent_bindings.kagent_create_request_id`, `kagent_create_agent_namespace`/`_name`). They stay set once the Session id is stored and are cleared only when a create is settled or the Session is replaced, so every reader (the orphan sweep, the resolve sweep) also requires `kagent_session_id IS NULL`. A `resolve-session-creates` sweep (every 5 minutes) settles such a binding whose session has no open delivery: it first rotates the binding's `kagent_request_id` (one UPDATE, fenced on the binding still being unbound, the same create request id and no open delivery), so a message that arrives mid-sweep creates its own Session under the new id and the sweep only deletes the one its replay returns. It then replays the create under the old id with the Agent stored on the binding and the same credential reference the create sent. kagent answering that the id belongs to a deleted Session settles it; `AlreadyExists` (a contract mismatch) also settles it, with a warning, leaving the Session to the orphan sweep; a Session that comes back is deleted (confirmed `DELETED`) first. A later send then finds the id deleted and replaces it as above. A delivery stuck because its Agent cannot be resolved logs at error level once past the uncertain hold; one waiting for a workspace that never gets a Session fails after that hold. A workspace's Session is only ever resumed through the workspace service (`Wake`), never directly. A native or main session follows the configured Agent: a create under a new request id uses it, and so does a replacement Session. A configuration change between a lost create reply and the next send does not fail the message: the send replays the create under the old request id with the new Agent, kagent answers `AlreadyExists`, and the turn treats that like a deleted request id and replaces it, so the message completes on a new Session on the renamed Agent. The Session that landed on the old Agent is then an orphan: every Agent a create was ever sent to is kept (`kagent_agents`, written with the marker and with a workspace), so the orphan sweep deletes it. +- A task-owned session may start with its `/model` and `/effort` override already set (#318): `TaskCreate` and the `delegate` MCP tool take optional `model` and `effort`, validated against the resolved provider profile's `models`/`effort_levels` at task creation (`422` with the allowed values otherwise; see [delegation](delegation.md)), recorded on the task and its attempt, and applied with `SetModelOverride`/`SetEffortOverride` — the same storage and turn metadata as a direct `/model`/`/effort` write — before the attempt's first brief is sent, never after. Leaving both unset leaves the session's override unset too, unchanged from before #318. The chat composer and the `/model`/`/effort` commands ([chat](chat.md#the-model-and-effort-commands)) read and change the same override on a task-owned session as on any other. - The main session cannot be archived (a database trigger refuses it). At most one main binding exists in the whole database; the owner is a single user for now, so the index is global. - A task that finished while no process was watching is finalized from `GetTask` on restart. A task parked for input keeps the turn open. - Failure reasons are stored with best-effort redaction and a 300 character cut (see [chat](chat.md)). diff --git a/server/internal/api/api.gen.go b/server/internal/api/api.gen.go index ba33cfe1..937b35d7 100644 --- a/server/internal/api/api.gen.go +++ b/server/internal/api/api.gen.go @@ -5011,21 +5011,27 @@ type Task struct { CreatedAt time.Time `json:"created_at"` CreatorBindingId *string `json:"creator_binding_id"` CurrentAttemptId *string `json:"current_attempt_id"` - Id string `json:"id"` - Mode TaskMode `json:"mode"` - OwnerDecision *OwnerDecision `json:"owner_decision"` - OwnerId string `json:"owner_id"` - ParentTaskId *string `json:"parent_task_id"` - ProjectId *string `json:"project_id"` - ProviderConstraint *string `json:"provider_constraint"` - Reason *TaskReason `json:"reason"` - RootTaskId string `json:"root_task_id"` - SelectionSource TaskSelectionSource `json:"selection_source"` - Status TaskStatus `json:"status"` - Title string `json:"title"` - TopicId *string `json:"topic_id"` - UpdatedAt time.Time `json:"updated_at"` - Version int `json:"version"` + + // Effort The reasoning effort the create request chose, validated against assigned_profile_id. Null means the profile's default_effort. + Effort *EffortLevel `json:"effort"` + Id string `json:"id"` + Mode TaskMode `json:"mode"` + + // Model The model the create request chose, validated against assigned_profile_id. Null means the profile's default_model. + Model *string `json:"model"` + OwnerDecision *OwnerDecision `json:"owner_decision"` + OwnerId string `json:"owner_id"` + ParentTaskId *string `json:"parent_task_id"` + ProjectId *string `json:"project_id"` + ProviderConstraint *string `json:"provider_constraint"` + Reason *TaskReason `json:"reason"` + RootTaskId string `json:"root_task_id"` + SelectionSource TaskSelectionSource `json:"selection_source"` + Status TaskStatus `json:"status"` + Title string `json:"title"` + TopicId *string `json:"topic_id"` + UpdatedAt time.Time `json:"updated_at"` + Version int `json:"version"` } // TaskMode defines model for Task.Mode. @@ -5061,35 +5067,41 @@ type TaskArtifactKind string // TaskAttempt defines model for TaskAttempt. type TaskAttempt struct { - AgentRef ProviderAgentRef `json:"agent_ref"` - ArchivedAt *time.Time `json:"archived_at"` - BindingId *string `json:"binding_id"` - BriefDeliveryId *string `json:"brief_delivery_id"` - CheckpointRef *string `json:"checkpoint_ref"` - ConfigurationRevision string `json:"configuration_revision"` - CreatedAt time.Time `json:"created_at"` - Depth TaskAttemptDepth `json:"depth"` - Environment *WorkspaceEnvironment `json:"environment"` - EvidenceRefs []string `json:"evidence_refs"` - Id string `json:"id"` - InitialRef *string `json:"initial_ref"` - ManifestRef *string `json:"manifest_ref"` - NativeDeletedAt *time.Time `json:"native_deleted_at"` - NativeProvider TaskAttemptNativeProvider `json:"native_provider"` - Number int `json:"number"` - PredecessorId *string `json:"predecessor_id"` - ProfileId string `json:"profile_id"` - ResultRef *string `json:"result_ref"` - RetentionHold *string `json:"retention_hold"` - Role TaskAttemptRole `json:"role"` - SessionId *string `json:"session_id"` - State TaskAttemptState `json:"state"` - SuccessorId *string `json:"successor_id"` - SupersededAt *time.Time `json:"superseded_at"` - TaskId string `json:"task_id"` - UpdatedAt time.Time `json:"updated_at"` - WorkspaceId *string `json:"workspace_id"` - WriterGeneration *int `json:"writer_generation"` + AgentRef ProviderAgentRef `json:"agent_ref"` + ArchivedAt *time.Time `json:"archived_at"` + BindingId *string `json:"binding_id"` + BriefDeliveryId *string `json:"brief_delivery_id"` + CheckpointRef *string `json:"checkpoint_ref"` + ConfigurationRevision string `json:"configuration_revision"` + CreatedAt time.Time `json:"created_at"` + Depth TaskAttemptDepth `json:"depth"` + + // Effort The reasoning effort this attempt started with, or null for the profile's default. + Effort *EffortLevel `json:"effort"` + Environment *WorkspaceEnvironment `json:"environment"` + EvidenceRefs []string `json:"evidence_refs"` + Id string `json:"id"` + InitialRef *string `json:"initial_ref"` + ManifestRef *string `json:"manifest_ref"` + + // Model The model this attempt started with, or null for the profile's default. + Model *string `json:"model"` + NativeDeletedAt *time.Time `json:"native_deleted_at"` + NativeProvider TaskAttemptNativeProvider `json:"native_provider"` + Number int `json:"number"` + PredecessorId *string `json:"predecessor_id"` + ProfileId string `json:"profile_id"` + ResultRef *string `json:"result_ref"` + RetentionHold *string `json:"retention_hold"` + Role TaskAttemptRole `json:"role"` + SessionId *string `json:"session_id"` + State TaskAttemptState `json:"state"` + SuccessorId *string `json:"successor_id"` + SupersededAt *time.Time `json:"superseded_at"` + TaskId string `json:"task_id"` + UpdatedAt time.Time `json:"updated_at"` + WorkspaceId *string `json:"workspace_id"` + WriterGeneration *int `json:"writer_generation"` } // TaskAttemptDepth defines model for TaskAttempt.Depth. @@ -5132,14 +5144,20 @@ type TaskCheckoutOutput struct { // TaskCreate defines model for TaskCreate. type TaskCreate struct { - Brief string `json:"brief"` - Checkout *TaskCheckout `json:"checkout,omitempty"` - Mode TaskCreateMode `json:"mode"` - ProjectId *string `json:"project_id,omitempty"` - ProviderProfileId *string `json:"provider_profile_id,omitempty"` - RequestId string `json:"request_id"` - Title string `json:"title"` - TopicId *string `json:"topic_id,omitempty"` + Brief string `json:"brief"` + Checkout *TaskCheckout `json:"checkout,omitempty"` + + // Effort One of the resolved provider profile's effort_levels. Unset keeps the profile's default_effort. 422 with the allowed levels if the profile does not offer it. + Effort *EffortLevel `json:"effort,omitempty"` + Mode TaskCreateMode `json:"mode"` + + // Model One of the resolved provider profile's models. Unset keeps the profile's default_model. 422 with the allowed models if the profile does not offer it. + Model *string `json:"model,omitempty"` + ProjectId *string `json:"project_id,omitempty"` + ProviderProfileId *string `json:"provider_profile_id,omitempty"` + RequestId string `json:"request_id"` + Title string `json:"title"` + TopicId *string `json:"topic_id,omitempty"` } // TaskCreateMode defines model for TaskCreate.Mode. @@ -5162,6 +5180,12 @@ type TaskError struct { // TaskErrorDetail Task error payload. `reason` is always present; some reasons add detail fields. type TaskErrorDetail struct { + // AllowedEffortLevels effort_unavailable only: the resolved profile's offered effort levels. + AllowedEffortLevels *[]EffortLevel `json:"allowed_effort_levels,omitempty"` + + // AllowedModels model_unavailable only: the resolved profile's offered model ids. + AllowedModels *[]string `json:"allowed_models,omitempty"` + // HeldTasks parent_capacity only: tasks holding the capacity. HeldTasks *[]TaskCapacityHolder `json:"held_tasks,omitempty"` @@ -6576,6 +6600,22 @@ func (a *TaskErrorDetail) UnmarshalJSON(b []byte) error { return err } + if raw, found := object["allowed_effort_levels"]; found { + err = json.Unmarshal(raw, &a.AllowedEffortLevels) + if err != nil { + return fmt.Errorf("error reading 'allowed_effort_levels': %w", err) + } + delete(object, "allowed_effort_levels") + } + + if raw, found := object["allowed_models"]; found { + err = json.Unmarshal(raw, &a.AllowedModels) + if err != nil { + return fmt.Errorf("error reading 'allowed_models': %w", err) + } + delete(object, "allowed_models") + } + if raw, found := object["held_tasks"]; found { err = json.Unmarshal(raw, &a.HeldTasks) if err != nil { @@ -6627,6 +6667,20 @@ func (a TaskErrorDetail) MarshalJSON() ([]byte, error) { var err error object := make(map[string]json.RawMessage) + if a.AllowedEffortLevels != nil { + object["allowed_effort_levels"], err = json.Marshal(a.AllowedEffortLevels) + if err != nil { + return nil, fmt.Errorf("error marshaling 'allowed_effort_levels': %w", err) + } + } + + if a.AllowedModels != nil { + object["allowed_models"], err = json.Marshal(a.AllowedModels) + if err != nil { + return nil, fmt.Errorf("error marshaling 'allowed_models': %w", err) + } + } + if a.HeldTasks != nil { object["held_tasks"], err = json.Marshal(a.HeldTasks) if err != nil { @@ -22154,239 +22208,243 @@ var swaggerSpec = []string{ "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/server/internal/apierr/apierr.go b/server/internal/apierr/apierr.go index c8c59e2d..67d9f3c8 100644 --- a/server/internal/apierr/apierr.go +++ b/server/internal/apierr/apierr.go @@ -50,9 +50,7 @@ type validationBody struct { } type reasonBody struct { - Detail struct { - Reason string `json:"reason"` - } `json:"detail"` + Detail map[string]any `json:"detail"` } // New builds an error with the contract's HTTPError body. @@ -95,9 +93,17 @@ func Invalid(msg string) *Error { // Reason builds an error with the contract's TaskError body: a stable machine code the client // switches on. func Reason(status int, reason string) *Error { - var b reasonBody - b.Detail.Reason = reason - return &Error{Status: status, Message: reason, body: b} + return ReasonDetail(status, reason, nil) +} + +// ReasonDetail is [Reason] with extra TaskErrorDetail fields alongside reason, for example the +// allowed values of a 422 the client can show. +func ReasonDetail(status int, reason string, extra map[string]any) *Error { + detail := map[string]any{"reason": reason} + for k, v := range extra { + detail[k] = v + } + return &Error{Status: status, Message: reason, body: reasonBody{Detail: detail}} } // NotImplemented is returned by operations no slice has implemented yet. diff --git a/server/internal/db/dbq/models.go b/server/internal/db/dbq/models.go index a1cb66f9..7f4366e7 100644 --- a/server/internal/db/dbq/models.go +++ b/server/internal/db/dbq/models.go @@ -740,6 +740,8 @@ type Task struct { CreatedAt time.Time UpdatedAt time.Time MergeProposalID *string + Model *string + Effort *string } type TaskArtifact struct { @@ -795,6 +797,8 @@ type TaskAttempt struct { ProgressAt *time.Time StopRequestedAt *time.Time LivenessFailingSince *time.Time + Model *string + Effort *string } type TaskEventDelivery struct { diff --git a/server/internal/db/dbq/tasks.sql.go b/server/internal/db/dbq/tasks.sql.go index b45eb979..90375152 100644 --- a/server/internal/db/dbq/tasks.sql.go +++ b/server/internal/db/dbq/tasks.sql.go @@ -96,7 +96,7 @@ UPDATE task_attempts SET session_id = $2, binding_id = $2, workspace_id = $2, environment = coalesce(environment, $3::jsonb), updated_at = now() WHERE id = $1 AND state = 'creating' AND (session_id IS NULL OR session_id = $2) -RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since +RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort ` type AttachAttemptSessionParams struct { @@ -153,6 +153,8 @@ func (q *Queries) AttachAttemptSession(ctx context.Context, arg AttachAttemptSes &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } @@ -227,7 +229,7 @@ UPDATE tasks SET status = 'completed', reason = NULL, decision_attempt_id = NULL, decision_report_id = NULL, decision_action = NULL, merge_proposal_id = $3, version = version + 1, updated_at = now() WHERE id = $1 AND version = $2 -RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id +RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort ` type CompleteTaskMergedParams struct { @@ -277,6 +279,8 @@ func (q *Queries) CompleteTaskMerged(ctx context.Context, arg CompleteTaskMerged &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } @@ -292,7 +296,7 @@ WHERE task_attempts.id = $1 AND task_attempts.state = 'draining' ) AND (SELECT coalesce(sum(d.outcome_version), 0)::bigint FROM deliveries d WHERE d.session_id = task_attempts.session_id) = $2::bigint -RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since +RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort ` type ConfirmAttemptStopParams struct { @@ -354,6 +358,8 @@ func (q *Queries) ConfirmAttemptStop(ctx context.Context, arg ConfirmAttemptStop &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } @@ -604,7 +610,7 @@ func (q *Queries) GetProjectFullName(ctx context.Context, id string) (string, er } const getTask = `-- name: GetTask :one -SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id FROM tasks WHERE id = $1 AND owner_id = $2 +SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort FROM tasks WHERE id = $1 AND owner_id = $2 ` type GetTaskParams struct { @@ -652,13 +658,15 @@ func (q *Queries) GetTask(ctx context.Context, arg GetTaskParams) (Task, error) &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } const getTaskAttempt = `-- name: GetTaskAttempt :one -SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since FROM task_attempts WHERE id = $1 +SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort FROM task_attempts WHERE id = $1 ` // Attempts @@ -709,13 +717,15 @@ func (q *Queries) GetTaskAttempt(ctx context.Context, id string) (TaskAttempt, e &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } const getTaskByID = `-- name: GetTaskByID :one -SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id FROM tasks WHERE id = $1 +SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort FROM tasks WHERE id = $1 ` // Internal lookups for the reconcile handlers, which know a row id and not the caller. @@ -759,12 +769,14 @@ func (q *Queries) GetTaskByID(ctx context.Context, id string) (Task, error) { &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } const getTaskByIDForUpdate = `-- name: GetTaskByIDForUpdate :one -SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id FROM tasks WHERE id = $1 FOR UPDATE +SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort FROM tasks WHERE id = $1 FOR UPDATE ` func (q *Queries) GetTaskByIDForUpdate(ctx context.Context, id string) (Task, error) { @@ -807,6 +819,8 @@ func (q *Queries) GetTaskByIDForUpdate(ctx context.Context, id string) (Task, er &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } @@ -849,7 +863,7 @@ func (q *Queries) GetTaskEventDelivery(ctx context.Context, id string) (TaskEven } const getTaskForUpdate = `-- name: GetTaskForUpdate :one -SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id FROM tasks WHERE id = $1 AND owner_id = $2 FOR UPDATE +SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort FROM tasks WHERE id = $1 AND owner_id = $2 FOR UPDATE ` type GetTaskForUpdateParams struct { @@ -897,6 +911,8 @@ func (q *Queries) GetTaskForUpdate(ctx context.Context, arg GetTaskForUpdatePara &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } @@ -1115,11 +1131,11 @@ const insertTask = `-- name: InsertTask :one INSERT INTO tasks ( id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, checkout_branch, checkout_ref, - checkout_depth + checkout_depth, model, effort ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) -RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id +RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort ` type InsertTaskParams struct { @@ -1139,11 +1155,14 @@ type InsertTaskParams struct { CheckoutBranch *string CheckoutRef *string CheckoutDepth *int32 + Model *string + Effort *string } // Tasks // reason stays NULL: a new task has nothing wrong with it yet, and provisioning has not been -// attempted. See Service.Create. +// attempted. See Service.Create. model and effort are the create request's own, already +// validated against the resolved profile; NULL means the profile's default. func (q *Queries) InsertTask(ctx context.Context, arg InsertTaskParams) (Task, error) { row := q.db.QueryRow(ctx, insertTask, arg.ID, @@ -1162,6 +1181,8 @@ func (q *Queries) InsertTask(ctx context.Context, arg InsertTaskParams) (Task, e arg.CheckoutBranch, arg.CheckoutRef, arg.CheckoutDepth, + arg.Model, + arg.Effort, ) var i Task err := row.Scan( @@ -1201,6 +1222,8 @@ func (q *Queries) InsertTask(ctx context.Context, arg InsertTaskParams) (Task, e &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } @@ -1209,11 +1232,12 @@ const insertTaskAttempt = `-- name: InsertTaskAttempt :one INSERT INTO task_attempts ( id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, - role, depth, writer_generation, initial_ref, session_id, binding_id, workspace_id, workspace_request_id + role, depth, writer_generation, initial_ref, session_id, binding_id, workspace_id, workspace_request_id, + model, effort ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) -RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since +RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort ` type InsertTaskAttemptParams struct { @@ -1233,10 +1257,14 @@ type InsertTaskAttemptParams struct { BindingID *string WorkspaceID *string WorkspaceRequestID *string + Model *string + Effort *string } // Provisioning and settlement. The attempt, the task's pointer to it and the writer claim change // in one transaction under the task row lock. +// model and effort are copied from the task row: the create request's own choice, already +// validated against the resolved profile, or NULL for the profile's default. func (q *Queries) InsertTaskAttempt(ctx context.Context, arg InsertTaskAttemptParams) (TaskAttempt, error) { row := q.db.QueryRow(ctx, insertTaskAttempt, arg.ID, @@ -1255,6 +1283,8 @@ func (q *Queries) InsertTaskAttempt(ctx context.Context, arg InsertTaskAttemptPa arg.BindingID, arg.WorkspaceID, arg.WorkspaceRequestID, + arg.Model, + arg.Effort, ) var i TaskAttempt err := row.Scan( @@ -1301,6 +1331,8 @@ func (q *Queries) InsertTaskAttempt(ctx context.Context, arg InsertTaskAttemptPa &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } @@ -1449,7 +1481,7 @@ func (q *Queries) InsertTaskReport(ctx context.Context, arg InsertTaskReportPara } const listActiveAttemptsForLiveness = `-- name: ListActiveAttemptsForLiveness :many -SELECT a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since FROM task_attempts a +SELECT a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since, a.model, a.effort FROM task_attempts a JOIN tasks t ON t.id = a.task_id WHERE a.state = 'active' AND t.status = 'running' AND t.current_attempt_id = a.id ORDER BY a.updated_at, a.id @@ -1512,6 +1544,8 @@ func (q *Queries) ListActiveAttemptsForLiveness(ctx context.Context, limit int32 &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -1524,7 +1558,7 @@ func (q *Queries) ListActiveAttemptsForLiveness(ctx context.Context, limit int32 } const listAttemptWorkspaceCleanups = `-- name: ListAttemptWorkspaceCleanups :many -SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since FROM task_attempts +SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort FROM task_attempts WHERE state IN ('superseded', 'failed', 'cancelled', 'completed') AND workspace_request_id IS NOT NULL AND brief_delivery_id IS NULL AND NOT workspace_cleanup_confirmed AND updated_at < now() - make_interval(secs => $2::float8) @@ -1593,6 +1627,8 @@ func (q *Queries) ListAttemptWorkspaceCleanups(ctx context.Context, arg ListAtte &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -1605,7 +1641,7 @@ func (q *Queries) ListAttemptWorkspaceCleanups(ctx context.Context, arg ListAtte } const listChildTasks = `-- name: ListChildTasks :many -SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id FROM tasks WHERE owner_id = $1 AND parent_task_id = $2 ORDER BY created_at, id +SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort FROM tasks WHERE owner_id = $1 AND parent_task_id = $2 ORDER BY created_at, id ` type ListChildTasksParams struct { @@ -1659,6 +1695,8 @@ func (q *Queries) ListChildTasks(ctx context.Context, arg ListChildTasksParams) &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -1671,7 +1709,7 @@ func (q *Queries) ListChildTasks(ctx context.Context, arg ListChildTasksParams) } const listChildTasksForUpdate = `-- name: ListChildTasksForUpdate :many -SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id FROM tasks WHERE parent_task_id = $1 ORDER BY id FOR UPDATE +SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort FROM tasks WHERE parent_task_id = $1 ORDER BY id FOR UPDATE ` // Child tasks in id order, locked. Settling a supervisor takes its task, then these, then its @@ -1722,6 +1760,8 @@ func (q *Queries) ListChildTasksForUpdate(ctx context.Context, parentTaskID *str &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -1734,7 +1774,7 @@ func (q *Queries) ListChildTasksForUpdate(ctx context.Context, parentTaskID *str } const listFailedBriefAttempts = `-- name: ListFailedBriefAttempts :many -SELECT a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since FROM task_attempts a +SELECT a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since, a.model, a.effort FROM task_attempts a JOIN tasks t ON t.id = a.task_id JOIN deliveries d ON d.id = a.brief_delivery_id WHERE a.state = 'active' AND t.status NOT IN ('completed', 'failed', 'cancelled') @@ -1799,6 +1839,8 @@ func (q *Queries) ListFailedBriefAttempts(ctx context.Context, limit int32) ([]T &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -1908,7 +1950,7 @@ func (q *Queries) ListOpenOperationsForSource(ctx context.Context, sourceAttempt const listProjectionCandidates = `-- name: ListProjectionCandidates :many -SELECT t.id, t.owner_id, t.project_id, t.topic_id, t.parent_task_id, t.root_task_id, t.creator_binding_id, t.title, t.brief, t.mode, t.assigned_profile_id, t.selection_source, t.provider_constraint, t.accepted_environment, t.status, t.reason, t.decision_attempt_id, t.decision_report_id, t.decision_action, t.current_attempt_id, t.version, t.checkout_branch, t.checkout_ref, t.checkout_depth, t.pr_number, t.pr_url, t.pr_head_sha, t.pr_repository, t.pr_description, t.pr_state, t.ci_state, t.ci_head_sha, t.projected_at, t.created_at, t.updated_at, t.merge_proposal_id, p.full_name AS project_full_name +SELECT t.id, t.owner_id, t.project_id, t.topic_id, t.parent_task_id, t.root_task_id, t.creator_binding_id, t.title, t.brief, t.mode, t.assigned_profile_id, t.selection_source, t.provider_constraint, t.accepted_environment, t.status, t.reason, t.decision_attempt_id, t.decision_report_id, t.decision_action, t.current_attempt_id, t.version, t.checkout_branch, t.checkout_ref, t.checkout_depth, t.pr_number, t.pr_url, t.pr_head_sha, t.pr_repository, t.pr_description, t.pr_state, t.ci_state, t.ci_head_sha, t.projected_at, t.created_at, t.updated_at, t.merge_proposal_id, t.model, t.effort, p.full_name AS project_full_name FROM tasks t JOIN projects p ON p.id = t.project_id WHERE t.mode = 'code' AND t.status NOT IN ('completed', 'failed', 'cancelled') @@ -1960,6 +2002,8 @@ type ListProjectionCandidatesRow struct { CreatedAt time.Time UpdatedAt time.Time MergeProposalID *string + Model *string + Effort *string ProjectFullName string } @@ -2011,6 +2055,8 @@ func (q *Queries) ListProjectionCandidates(ctx context.Context, arg ListProjecti &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, &i.ProjectFullName, ); err != nil { return nil, err @@ -2159,7 +2205,7 @@ func (q *Queries) ListTaskArtifacts(ctx context.Context, taskIds []string) ([]Ta } const listTaskAttempts = `-- name: ListTaskAttempts :many -SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since FROM task_attempts WHERE task_id = ANY($1::text[]) ORDER BY task_id, number +SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort FROM task_attempts WHERE task_id = ANY($1::text[]) ORDER BY task_id, number ` func (q *Queries) ListTaskAttempts(ctx context.Context, taskIds []string) ([]TaskAttempt, error) { @@ -2215,6 +2261,8 @@ func (q *Queries) ListTaskAttempts(ctx context.Context, taskIds []string) ([]Tas &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -2347,7 +2395,7 @@ func (q *Queries) ListTaskReports(ctx context.Context, taskIds []string) ([]Task } const listTasks = `-- name: ListTasks :many -SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id FROM tasks +SELECT id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort FROM tasks WHERE owner_id = $1 AND ($2::text IS NULL OR project_id = $2) AND ($3::text IS NULL OR parent_task_id = $3) @@ -2406,6 +2454,8 @@ func (q *Queries) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task, e &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -2471,7 +2521,7 @@ func (q *Queries) ListUnresolvedStops(ctx context.Context, ownerID string) ([]Li } const listUnsettledAttempts = `-- name: ListUnsettledAttempts :many -SELECT a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since FROM task_attempts a +SELECT a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since, a.model, a.effort FROM task_attempts a JOIN tasks t ON t.id = a.task_id WHERE (a.state IN ('draining', 'fenced') OR (a.state IN ('creating', 'active') AND t.status IN ('completed', 'failed', 'cancelled'))) @@ -2542,6 +2592,8 @@ func (q *Queries) ListUnsettledAttempts(ctx context.Context, arg ListUnsettledAt &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ); err != nil { return nil, err } @@ -2576,7 +2628,7 @@ func (q *Queries) LockParentCurrentAttemptShared(ctx context.Context, id string) } const lockTaskAttemptShared = `-- name: LockTaskAttemptShared :one -SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since FROM task_attempts WHERE id = $1 FOR SHARE +SELECT id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort FROM task_attempts WHERE id = $1 FOR SHARE ` // Locks an attempt against being moved while the caller's write commits, and reads its state. @@ -2627,6 +2679,8 @@ func (q *Queries) LockTaskAttemptShared(ctx context.Context, id string) (TaskAtt &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } @@ -2674,7 +2728,7 @@ const markAttemptStopUnresolved = `-- name: MarkAttemptStopUnresolved :one UPDATE task_attempts SET stop_state = 'unresolved', updated_at = now() WHERE id = $1 AND state = 'draining' AND stop_state = 'requested' -RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since +RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort ` // The resolver has looked the bound number of times. Returns no row when it was already marked, so @@ -2726,6 +2780,8 @@ func (q *Queries) MarkAttemptStopUnresolved(ctx context.Context, id string) (Tas &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } @@ -2739,7 +2795,7 @@ SET state = $1, capacity_held = $2, updated_at = now(), evidence_refs = CASE WHEN $3::text IS NULL THEN evidence_refs ELSE array_append(evidence_refs, $3::text) END WHERE id = $4 AND state = $5 -RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since +RETURNING id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, role, depth, state, capacity_held, session_id, binding_id, workspace_id, workspace_request_id, workspace_creates_inflight, workspace_cleanup_confirmed, workspace_cleanup_attempts, workspace_cleanup_after, writer_generation, brief_delivery_id, predecessor_id, successor_id, evidence_refs, result_ref, environment, initial_ref, checkpoint_ref, manifest_ref, superseded_at, archived_at, native_deleted_at, retention_hold, stop_state, resolve_attempts, resolve_after, created_at, updated_at, progress_marker, progress_at, stop_requested_at, liveness_failing_since, model, effort ` type MoveAttemptParams struct { @@ -2805,6 +2861,8 @@ func (q *Queries) MoveAttempt(ctx context.Context, arg MoveAttemptParams) (TaskA &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } @@ -2962,7 +3020,7 @@ WHERE a.id = $1 AND a.state = 'creating' AND a.workspace_request_id IS NOT NULL AND EXISTS (SELECT 1 FROM task_operations o WHERE o.id = $2 AND o.target_attempt_id = a.id AND o.state = 'target_creating' AND o.lease_owner = $3 AND o.attempts = $4) -RETURNING a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since +RETURNING a.id, a.task_id, a.number, a.profile_id, a.native_provider, a.configuration_revision, a.agent_namespace, a.agent_name, a.role, a.depth, a.state, a.capacity_held, a.session_id, a.binding_id, a.workspace_id, a.workspace_request_id, a.workspace_creates_inflight, a.workspace_cleanup_confirmed, a.workspace_cleanup_attempts, a.workspace_cleanup_after, a.writer_generation, a.brief_delivery_id, a.predecessor_id, a.successor_id, a.evidence_refs, a.result_ref, a.environment, a.initial_ref, a.checkpoint_ref, a.manifest_ref, a.superseded_at, a.archived_at, a.native_deleted_at, a.retention_hold, a.stop_state, a.resolve_attempts, a.resolve_after, a.created_at, a.updated_at, a.progress_marker, a.progress_at, a.stop_requested_at, a.liveness_failing_since, a.model, a.effort ` type ReserveAttemptWorkspaceCreateParams struct { @@ -3026,6 +3084,8 @@ func (q *Queries) ReserveAttemptWorkspaceCreate(ctx context.Context, arg Reserve &i.ProgressAt, &i.StopRequestedAt, &i.LivenessFailingSince, + &i.Model, + &i.Effort, ) return i, err } @@ -3152,7 +3212,7 @@ SET current_attempt_id = $1, status = $2, reason = $3, decision_action = CASE WHEN $2 = 'blocked' THEN decision_action END, version = version + 1, updated_at = now() WHERE id = $5 -RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id +RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort ` type SetTaskAttemptParams struct { @@ -3209,6 +3269,8 @@ func (q *Queries) SetTaskAttempt(ctx context.Context, arg SetTaskAttemptParams) &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } @@ -3360,7 +3422,7 @@ SET pr_number = $2, pr_url = $3, pr_head_sha = $4, pr_state = $5, ci_state = $6, pr_repository = $8, pr_description = $9, projected_at = $10, version = version + 1, updated_at = now() WHERE id = $1 -RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id +RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort ` type SetTaskProjectionParams struct { @@ -3427,6 +3489,8 @@ func (q *Queries) SetTaskProjection(ctx context.Context, arg SetTaskProjectionPa &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } @@ -3443,7 +3507,7 @@ func (q *Queries) TouchAttemptLiveness(ctx context.Context, id string) error { } const touchTask = `-- name: TouchTask :one -UPDATE tasks SET version = version + 1, updated_at = now() WHERE id = $1 AND version = $2 RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id +UPDATE tasks SET version = version + 1, updated_at = now() WHERE id = $1 AND version = $2 RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort ` type TouchTaskParams struct { @@ -3492,6 +3556,8 @@ func (q *Queries) TouchTask(ctx context.Context, arg TouchTaskParams) (Task, err &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } @@ -3522,7 +3588,7 @@ SET status = $3, version = version + 1, updated_at = now() WHERE id = $1 AND version = $2 -RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id +RETURNING id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, accepted_environment, status, reason, decision_attempt_id, decision_report_id, decision_action, current_attempt_id, version, checkout_branch, checkout_ref, checkout_depth, pr_number, pr_url, pr_head_sha, pr_repository, pr_description, pr_state, ci_state, ci_head_sha, projected_at, created_at, updated_at, merge_proposal_id, model, effort ` type UpdateTaskStateParams struct { @@ -3585,6 +3651,8 @@ func (q *Queries) UpdateTaskState(ctx context.Context, arg UpdateTaskStateParams &i.CreatedAt, &i.UpdatedAt, &i.MergeProposalID, + &i.Model, + &i.Effort, ) return i, err } diff --git a/server/internal/db/migrations/00340_task_model_effort.sql b/server/internal/db/migrations/00340_task_model_effort.sql new file mode 100644 index 00000000..db444673 --- /dev/null +++ b/server/internal/db/migrations/00340_task_model_effort.sql @@ -0,0 +1,44 @@ +-- +goose Up +-- A task's delegate-chosen model and reasoning effort: the create request's own choice (#318), +-- validated against the resolved provider profile's models/effort_levels before the task is +-- recorded. NULL means the profile's own default_model/default_effort, exactly as if the field +-- had never existed. tasks carries the create request's values so a later attempt (once handoff +-- is installed) can read them back; task_attempts carries what the attempt actually started with, +-- copied from the task when the attempt is made, and never changes after. +ALTER TABLE tasks ADD COLUMN model text + CHECK (model IS NULL OR model ~ '^[A-Za-z0-9._:/-]{1,200}$'); +ALTER TABLE tasks ADD COLUMN effort text + CHECK (effort IS NULL OR effort ~ '^[a-z]{1,20}$'); + +ALTER TABLE task_attempts ADD COLUMN model text + CHECK (model IS NULL OR model ~ '^[A-Za-z0-9._:/-]{1,200}$'); +ALTER TABLE task_attempts ADD COLUMN effort text + CHECK (effort IS NULL OR effort ~ '^[a-z]{1,20}$'); + +-- An attempt's model and effort are part of its routing, set once when it is made. +-- +goose StatementBegin +CREATE OR REPLACE FUNCTION task_attempt_routing_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF ROW(OLD.task_id, OLD.number, OLD.profile_id, OLD.native_provider, OLD.configuration_revision, OLD.agent_namespace, OLD.agent_name, OLD.role, OLD.depth, coalesce(OLD.environment, NEW.environment), OLD.initial_ref, OLD.workspace_request_id, OLD.model, OLD.effort) + IS DISTINCT FROM ROW(NEW.task_id, NEW.number, NEW.profile_id, NEW.native_provider, NEW.configuration_revision, NEW.agent_namespace, NEW.agent_name, NEW.role, NEW.depth, NEW.environment, NEW.initial_ref, NEW.workspace_request_id, NEW.model, NEW.effort) THEN + RAISE EXCEPTION 'attempt routing is immutable'; + END IF; + RETURN NEW; +END $$; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +CREATE OR REPLACE FUNCTION task_attempt_routing_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF ROW(OLD.task_id, OLD.number, OLD.profile_id, OLD.native_provider, OLD.configuration_revision, OLD.agent_namespace, OLD.agent_name, OLD.role, OLD.depth, coalesce(OLD.environment, NEW.environment), OLD.initial_ref, OLD.workspace_request_id) + IS DISTINCT FROM ROW(NEW.task_id, NEW.number, NEW.profile_id, NEW.native_provider, NEW.configuration_revision, NEW.agent_namespace, NEW.agent_name, NEW.role, NEW.depth, NEW.environment, NEW.initial_ref, NEW.workspace_request_id) THEN + RAISE EXCEPTION 'attempt routing is immutable'; + END IF; + RETURN NEW; +END $$; +-- +goose StatementEnd +ALTER TABLE task_attempts DROP COLUMN IF EXISTS effort; +ALTER TABLE task_attempts DROP COLUMN IF EXISTS model; +ALTER TABLE tasks DROP COLUMN IF EXISTS effort; +ALTER TABLE tasks DROP COLUMN IF EXISTS model; diff --git a/server/internal/db/queries/tasks.sql b/server/internal/db/queries/tasks.sql index 91dd5f5a..b825ac39 100644 --- a/server/internal/db/queries/tasks.sql +++ b/server/internal/db/queries/tasks.sql @@ -8,13 +8,14 @@ SELECT pg_advisory_xact_lock(hashtextextended(sqlc.arg(key)::text, 0)); -- name: InsertTask :one -- reason stays NULL: a new task has nothing wrong with it yet, and provisioning has not been --- attempted. See Service.Create. +-- attempted. See Service.Create. model and effort are the create request's own, already +-- validated against the resolved profile; NULL means the profile's default. INSERT INTO tasks ( id, owner_id, project_id, topic_id, parent_task_id, root_task_id, creator_binding_id, title, brief, mode, assigned_profile_id, selection_source, provider_constraint, checkout_branch, checkout_ref, - checkout_depth + checkout_depth, model, effort ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) RETURNING *; @@ -169,11 +170,14 @@ WHERE kind = 'create' AND target_attempt_id = $1 AND state IN ('target_creating' -- in one transaction under the task row lock. -- name: InsertTaskAttempt :one +-- model and effort are copied from the task row: the create request's own choice, already +-- validated against the resolved profile, or NULL for the profile's default. INSERT INTO task_attempts ( id, task_id, number, profile_id, native_provider, configuration_revision, agent_namespace, agent_name, - role, depth, writer_generation, initial_ref, session_id, binding_id, workspace_id, workspace_request_id + role, depth, writer_generation, initial_ref, session_id, binding_id, workspace_id, workspace_request_id, + model, effort ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) RETURNING *; diff --git a/server/internal/mcpserver/tools_tasks.go b/server/internal/mcpserver/tools_tasks.go index e76daf5b..bbe8a02c 100644 --- a/server/internal/mcpserver/tools_tasks.go +++ b/server/internal/mcpserver/tools_tasks.go @@ -25,7 +25,14 @@ func (s *Server) taskTools() []Tool { Name: agentauth.ToolDelegate, Description: "Create a durable task and scoped attempt. Reuse request_id for lost or uncertain responses. " + "After a confirmed terminal blocked create, use a NEW request_id for a new create; reusing the old id " + - "returns the blocked operation. Role and tool guidance prefixes the brief.", + "returns the blocked operation. Role and tool guidance prefixes the brief. " + + "Choosing model and effort: match the tier to the task's scope, risk and reversibility, not habit. " + + "Mechanical or bounded, easily-reversible work takes the cheapest model and lowest effort likely to " + + "succeed; design, debugging, security-sensitive review, or anything hard to undo takes the strongest " + + "model and highest effort the profile offers. When re-delegating a failed attempt, prefer a higher " + + "tier over repeating the same one. Read the resolved provider profile's own models and effort_levels " + + "(see the providers list) rather than assuming any provider's names; leaving both unset keeps the " + + "profile's defaults.", InputSchema: objectSchema(map[string]any{ "request_id": id, "title": stringSchema(1, 200), @@ -36,6 +43,12 @@ func (s *Server) taskTools() []Tool { "provider_profile_id": optional(map[string]any{ "type": "string", "minLength": 1, "maxLength": 80, "pattern": "^[a-z][a-z0-9_-]*$", }), + "model": optional(map[string]any{ + "type": "string", "pattern": "^[A-Za-z0-9._:/-]{1,200}$", + }), + "effort": optional(map[string]any{ + "type": "string", "enum": []any{"minimal", "low", "medium", "high", "xhigh", "max"}, + }), "checkout": optional(objectSchema(map[string]any{ "branch": stringSchema(1, 255), "ref": map[string]any{"type": "string", "maxLength": 255, "default": ""}, @@ -109,8 +122,13 @@ func taskFailure(err error) error { var r *tasks.Error if errors.As(err, &r) { reason := r.Reason - if reason == tasks.ReasonEnvironmentRequired { + switch reason { + case tasks.ReasonEnvironmentRequired: reason += ": the owner must select a development environment on the project page (Environment section), then delegate again" + case tasks.ReasonModelUnavailable: + reason += fmt.Sprintf(": allowed models are %v", r.Detail["allowed_models"]) + case tasks.ReasonEffortUnavailable: + reason += fmt.Sprintf(": allowed effort levels are %v", r.Detail["allowed_effort_levels"]) } return failure(strconv.Itoa(r.Status), reason) } @@ -152,6 +170,8 @@ func (s *Server) delegate(ctx context.Context, a agentauth.Actor, args json.RawM ProjectID *string `json:"project_id"` TopicID *string `json:"topic_id"` ProviderProfileID *string `json:"provider_profile_id"` + Model *string `json:"model"` + Effort *string `json:"effort"` Checkout *struct { Branch string `json:"branch"` Ref string `json:"ref"` @@ -162,7 +182,10 @@ func (s *Server) delegate(ctx context.Context, a agentauth.Actor, args json.RawM return Result{}, err } var err error - create := tasks.CreateInput{ProjectID: str(in.ProjectID), TopicID: str(in.TopicID), ProviderProfileID: str(in.ProviderProfileID)} + create := tasks.CreateInput{ + ProjectID: str(in.ProjectID), TopicID: str(in.TopicID), ProviderProfileID: str(in.ProviderProfileID), + Model: str(in.Model), Effort: str(in.Effort), + } if create.RequestID, err = text(in.RequestID, 1, 100); err != nil { return Result{}, err } diff --git a/server/internal/tasks/api.go b/server/internal/tasks/api.go index 110f1ead..3ead5b20 100644 --- a/server/internal/tasks/api.go +++ b/server/internal/tasks/api.go @@ -31,7 +31,7 @@ var actionRefusals = []int{http.StatusForbidden, http.StatusNotFound, http.Statu func taskReply(err error, declared ...int) error { var e *Error if errors.As(err, &e) && slices.Contains(declared, e.Status) { - return apierr.Reason(e.Status, e.Reason) + return apierr.ReasonDetail(e.Status, e.Reason, e.Detail) } return err } @@ -91,6 +91,7 @@ func (a *TasksAPI) CreateTask(ctx context.Context, req api.CreateTaskRequestObje in := CreateInput{ RequestID: body.RequestId, Title: body.Title, Brief: body.Brief, Mode: dbstate.TaskMode(body.Mode), ProjectID: deref(body.ProjectId), TopicID: deref(body.TopicId), ProviderProfileID: deref(body.ProviderProfileId), + Model: deref(body.Model), Effort: derefEffort(body.Effort), } if c := body.Checkout; c != nil { in.Checkout = &Checkout{Branch: c.Branch, Ref: deref(c.Ref)} @@ -112,6 +113,13 @@ func deref(s *string) string { return *s } +func derefEffort(e *api.EffortLevel) string { + if e == nil { + return "" + } + return string(*e) +} + // CancelTask implements api.StrictServerInterface. func (a *TasksAPI) CancelTask(ctx context.Context, req api.CancelTaskRequestObject) (api.CancelTaskResponseObject, error) { if a.Svc == nil { diff --git a/server/internal/tasks/errors.go b/server/internal/tasks/errors.go index 7b1e034c..9615e3e7 100644 --- a/server/internal/tasks/errors.go +++ b/server/internal/tasks/errors.go @@ -3,16 +3,24 @@ package tasks import "fmt" // Error is a refusal the caller is shown: an HTTP status and the stable machine code that goes in -// the task error body. Anything that is not an *Error is an internal failure. +// the task error body. Detail carries extra TaskErrorDetail fields, for example the allowed +// values of a model_unavailable or effort_unavailable refusal. Anything that is not an *Error is +// an internal failure. type Error struct { Status int Reason string + Detail map[string]any } func (e *Error) Error() string { return fmt.Sprintf("tasks: %d %s", e.Status, e.Reason) } func refusal(status int, reason string) *Error { return &Error{Status: status, Reason: reason} } +// refusalDetail is [refusal] with extra TaskErrorDetail fields. +func refusalDetail(status int, reason string, detail map[string]any) *Error { + return &Error{Status: status, Reason: reason, Detail: detail} +} + // Reasons. The set is the contract of the task routes and the MCP tools. const ( ReasonTaskNotFound = "task_not_found" @@ -46,4 +54,6 @@ const ( ReasonBriefTooLong = "brief_too_long" ReasonChildCapacity = "child_capacity" ReasonGlobalCapacity = "global_capacity" + ReasonModelUnavailable = "model_unavailable" + ReasonEffortUnavailable = "effort_unavailable" ) diff --git a/server/internal/tasks/provision.go b/server/internal/tasks/provision.go index c8926810..ec827893 100644 --- a/server/internal/tasks/provision.go +++ b/server/internal/tasks/provision.go @@ -38,6 +38,10 @@ type SessionRuntime interface { Cancel(ctx context.Context, sessionID string) (sessions.CancelResult, error) Complete(ctx context.Context, sessionID string) (sessions.CancelResult, error) Fail(ctx context.Context, sessionID string) (sessions.CancelResult, error) + // SetModelOverride and SetEffortOverride apply an attempt's model and effort as the session's + // own override, reusing the same storage and turn metadata as the owner's /model and /effort. + SetModelOverride(ctx context.Context, sessionID string, model *string) (sessions.ModelEffortState, error) + SetEffortOverride(ctx context.Context, sessionID string, effort *string) (sessions.ModelEffortState, error) } // Register adds the task loops and sweeps to the runner. Without Workspaces and Sessions the rows @@ -229,6 +233,7 @@ func (s *Service) beginCreate(ctx context.Context, c reconcile.Claim, op dbq.Tas ID: uuid.NewString(), TaskID: t.ID, Number: number, ProfileID: prof.ID, NativeProvider: dbstate.NativeProvider(prof.NativeProvider), ConfigurationRevision: prof.ConfigurationRevision, AgentNamespace: agent.Namespace, AgentName: agent.Name, Role: shape.role, Depth: shape.depth, + Model: t.Model, Effort: t.Effort, } var repo string if t.Mode == dbstate.TaskModeCode { @@ -577,6 +582,18 @@ func (s *Service) sendBrief(ctx context.Context, c reconcile.Claim, op dbq.TaskO if err != nil { return reconcile.Outcome{}, err } + // Apply the attempt's model and effort as the session's own override before anything is sent, + // so the first turn already carries them; setting the same value again on a replay is a no-op. + if a.Model != nil { + if _, err := s.Sessions.SetModelOverride(ctx, *a.SessionID, a.Model); err != nil { + return reconcile.Outcome{}, err + } + } + if a.Effort != nil { + if _, err := s.Sessions.SetEffortOverride(ctx, *a.SessionID, a.Effort); err != nil { + return reconcile.Outcome{}, err + } + } mid := briefMessageID(a.ID) if _, err := q.GetDelivery(ctx, mid); errors.Is(err, pgx.ErrNoRows) { _, err := s.Sessions.SendToAs(ctx, sessions.SourceMainloop, *a.SessionID, mid, DelegatedBrief(a.Role, t.Mode, t.Brief)) diff --git a/server/internal/tasks/provision_test.go b/server/internal/tasks/provision_test.go index fe41c519..61563a90 100644 --- a/server/internal/tasks/provision_test.go +++ b/server/internal/tasks/provision_test.go @@ -108,6 +108,40 @@ func TestDelegateProvisionsAndReports(t *testing.T) { } } +// TestDelegateAppliesModelAndEffortBeforeTheFirstTurn covers #318: a chosen model and effort are +// recorded on the attempt and applied as the session's own override (the same storage /model and +// /effort use) before the brief is sent, so the first turn already carries them. Leaving both +// unset leaves no override at all: unchanged behavior. +func TestDelegateAppliesModelAndEffortBeforeTheFirstTurn(t *testing.T) { + r := newRig(t) + r.run() + + root := r.mustCreate(r.own, tasks.CreateInput{ + ProjectID: "p1", Brief: "Ship the thing", Model: "claude-opus-5-5", Effort: "high", + }) + r.eventually("chosen supervisor provisioned", func() bool { return r.opState(root) == "completed" }) + attempt, binding := r.attemptOf(root) + if got := r.str(`SELECT model FROM task_attempts WHERE id = $1`, attempt); got != "claude-opus-5-5" { + t.Fatalf("attempt model = %q", got) + } + if got := r.str(`SELECT effort FROM task_attempts WHERE id = $1`, attempt); got != "high" { + t.Fatalf("attempt effort = %q", got) + } + if got := r.str(`SELECT model_override FROM agent_bindings WHERE session_id = $1`, binding); got != "claude-opus-5-5" { + t.Fatalf("binding model_override = %q, want it set before the first turn", got) + } + if got := r.str(`SELECT effort_override FROM agent_bindings WHERE session_id = $1`, binding); got != "high" { + t.Fatalf("binding effort_override = %q, want it set before the first turn", got) + } + + plain := r.mustCreate(r.own, tasks.CreateInput{ProjectID: "p1", Brief: "Plain"}) + r.eventually("plain supervisor provisioned", func() bool { return r.opState(plain) == "completed" }) + _, plainBinding := r.attemptOf(plain) + if n := r.count(`SELECT count(*) FROM agent_bindings WHERE session_id = $1 AND model_override IS NULL AND effort_override IS NULL`, plainBinding); n != 1 { + t.Fatalf("unset model/effort left an override on the binding") + } +} + func TestProvisionReplaysWithoutASecondWorkspace(t *testing.T) { r := newRig(t) root := r.mustCreate(r.own, tasks.CreateInput{ProjectID: "p1"}) diff --git a/server/internal/tasks/run_test.go b/server/internal/tasks/run_test.go index 7a4af379..90a83f81 100644 --- a/server/internal/tasks/run_test.go +++ b/server/internal/tasks/run_test.go @@ -18,6 +18,7 @@ import ( "github.com/oldsj/mainloop/server/internal/db/dbq" "github.com/oldsj/mainloop/server/internal/db/dbstate" "github.com/oldsj/mainloop/server/internal/kagent" + "github.com/oldsj/mainloop/server/internal/providers" "github.com/oldsj/mainloop/server/internal/reconcile" "github.com/oldsj/mainloop/server/internal/sessions" "github.com/oldsj/mainloop/server/internal/tasks" @@ -456,7 +457,10 @@ func newRig(t *testing.T) *rig { e := newEnv(t) r := &rig{env: e} r.ws = &fakeWorkspaces{pool: e.pool} - r.ses = &fakeSessions{Service: sessions.NewService(mustConfig(t), e.pool, nil, nil, nil, nil, nil, nil, slog.Default())} + // The same registry the task service resolves profiles from, so a session's /model and + // /effort (and an attempt's initial override) see the same models and effort_levels. + reg := e.svc.Providers.(*providers.Registry) //nolint:forcetypeassert // newEnv always builds one + r.ses = &fakeSessions{Service: sessions.NewService(mustConfig(t), e.pool, nil, reg, nil, nil, nil, nil, slog.Default())} r.ses.OwnerID = owner r.obs = &fakeObserver{} r.stops = &fakeStops{} diff --git a/server/internal/tasks/service.go b/server/internal/tasks/service.go index d06b8204..0bd6d2df 100644 --- a/server/internal/tasks/service.go +++ b/server/internal/tasks/service.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "log/slog" + "slices" "time" "github.com/google/uuid" @@ -164,7 +165,11 @@ type CreateInput struct { ProjectID string TopicID string ProviderProfileID string - Checkout *Checkout + // Model and Effort are the new attempt's starting overrides, validated against the resolved + // provider profile's Models and EffortLevels. Empty means the profile's own default. + Model string + Effort string + Checkout *Checkout } // Checkout is the repository position a code task starts from. @@ -178,7 +183,8 @@ func (in CreateInput) payload() map[string]any { m := map[string]any{ "title": in.Title, "brief": in.Brief, "mode": in.Mode, "project_id": nilIfEmpty(in.ProjectID), "topic_id": nilIfEmpty(in.TopicID), - "provider_profile_id": nilIfEmpty(in.ProviderProfileID), "checkout": nil, + "provider_profile_id": nilIfEmpty(in.ProviderProfileID), + "model": nilIfEmpty(in.Model), "effort": nilIfEmpty(in.Effort), "checkout": nil, } if in.Checkout != nil { m["checkout"] = map[string]any{"branch": in.Checkout.Branch, "ref": in.Checkout.Ref, "depth": in.Checkout.Depth} @@ -276,6 +282,27 @@ func (s *Service) selectProfile(ctx context.Context, q *dbq.Queries, in CreateIn return selection{profile: prof, source: dbstate.SelectionInstallationDefault}, nil } +// validateModelEffort checks a create request's model and effort against the resolved profile. +// Either left empty keeps the profile's own default_model/default_effort; given, it must be one +// of the profile's offered models or effort_levels, or the refusal carries the allowed values. +func validateModelEffort(in CreateInput, prof config.ProviderProfile) error { + if in.Model != "" && !slices.ContainsFunc(prof.Models, func(m config.ProviderModel) bool { return m.ID == in.Model }) { + return refusalDetail(422, ReasonModelUnavailable, map[string]any{"allowed_models": modelIDs(prof.Models)}) + } + if in.Effort != "" && !slices.Contains(prof.EffortLevels, in.Effort) { + return refusalDetail(422, ReasonEffortUnavailable, map[string]any{"allowed_effort_levels": prof.EffortLevels}) + } + return nil +} + +func modelIDs(models []config.ProviderModel) []string { + ids := make([]string, len(models)) + for i, m := range models { + ids[i] = m.ID + } + return ids +} + // Create records a task and its create operation, both with no reason: nothing has gone wrong // yet, and provisioning has not been attempted. It leaves the operation requested: the task has // no attempt until provisioning picks the operation up. A repeat of the same request returns the @@ -423,6 +450,9 @@ func (s *Service) insertTask(ctx context.Context, q *dbq.Queries, p Principal, i if err != nil { return dbq.Task{}, err } + if err := validateModelEffort(in, sel.profile); err != nil { + return dbq.Task{}, err + } id := uuid.NewString() root := id var parentID *string @@ -433,6 +463,7 @@ func (s *Service) insertTask(ctx context.Context, q *dbq.Queries, p Principal, i ID: id, OwnerID: p.OwnerID, ProjectID: project, TopicID: topic, ParentTaskID: parentID, RootTaskID: root, CreatorBindingID: ptr(p.BindingID), Title: in.Title, Brief: in.Brief, Mode: in.Mode, AssignedProfileID: sel.profile.ID, SelectionSource: sel.source, ProviderConstraint: sel.constraint, + Model: ptr(in.Model), Effort: ptr(in.Effort), } if c := in.Checkout; c != nil { depth := int32(c.Depth) //nolint:gosec // validate bounds it to 0..1000 diff --git a/server/internal/tasks/service_test.go b/server/internal/tasks/service_test.go index a2b4327e..b3f4a58d 100644 --- a/server/internal/tasks/service_test.go +++ b/server/internal/tasks/service_test.go @@ -297,6 +297,50 @@ func TestProviderSelection(t *testing.T) { } } +// TestCreateValidatesModelAndEffort covers #318: model and effort are optional, validated against +// the resolved profile's own models and effort_levels, persisted on the task when valid, and left +// unset (the profile's default, unchanged behavior) when the request names neither. +func TestCreateValidatesModelAndEffort(t *testing.T) { + e := newEnv(t) + + // Neither given: the task's model and effort stay unset, so the profile's own default applies, + // exactly as if the fields had never existed. + plain := e.view(e.own, e.mustCreate(e.own, tasks.CreateInput{ProjectID: "p1"})).Task + if plain.Model != nil || plain.Effort != nil { + t.Fatalf("model/effort = %v/%v, want unset", plain.Model, plain.Effort) + } + + // A model and effort the resolved (built-in claude) profile offers persist on the task. + chosen := e.view(e.own, e.mustCreate(e.own, tasks.CreateInput{ + ProjectID: "p1", Model: "claude-opus-5-5", Effort: "high", + })).Task + if chosen.Model == nil || *chosen.Model != "claude-opus-5-5" || chosen.Effort == nil || *chosen.Effort != "high" { + t.Fatalf("model/effort = %v/%v", chosen.Model, chosen.Effort) + } + + // A model the resolved profile does not offer is refused with the allowed models. + _, err := e.create(e.own, tasks.CreateInput{ProjectID: "p1", Model: "gpt-5.1-codex"}) + var refused *tasks.Error + if !errors.As(err, &refused) || refused.Status != 422 || refused.Reason != tasks.ReasonModelUnavailable { + t.Fatalf("err = %v", err) + } + want := []string{"claude-sonnet-5-5", "claude-opus-5-5", "claude-haiku-5-5", "claude-fable-5-1"} + if got, _ := refused.Detail["allowed_models"].([]string); !reflect.DeepEqual(got, want) { + t.Fatalf("allowed_models = %v, want %v", refused.Detail["allowed_models"], want) + } + + // An effort the resolved profile does not offer is refused with the allowed levels (codex's + // minimal is not one of claude's). + _, err = e.create(e.own, tasks.CreateInput{ProjectID: "p1", Effort: "minimal"}) + if !errors.As(err, &refused) || refused.Status != 422 || refused.Reason != tasks.ReasonEffortUnavailable { + t.Fatalf("err = %v", err) + } + wantEffort := []string{"low", "medium", "high", "xhigh", "max"} + if got, _ := refused.Detail["allowed_effort_levels"].([]string); !reflect.DeepEqual(got, wantEffort) { + t.Fatalf("allowed_effort_levels = %v, want %v", refused.Detail["allowed_effort_levels"], wantEffort) + } +} + func TestSupervisorCreatesChildren(t *testing.T) { e := newEnv(t) root := e.mustCreate(e.own, tasks.CreateInput{ProjectID: "p1", TopicID: "tp1", ProviderProfileID: "codex"}) diff --git a/server/internal/tasks/view.go b/server/internal/tasks/view.go index d75f80b5..96ab6e35 100644 --- a/server/internal/tasks/view.go +++ b/server/internal/tasks/view.go @@ -71,8 +71,10 @@ func toTask(t dbq.Task) (api.Task, error) { CreatedAt: t.CreatedAt, CreatorBindingId: t.CreatorBindingID, CurrentAttemptId: t.CurrentAttemptID, + Effort: (*api.EffortLevel)(t.Effort), Id: t.ID, Mode: api.TaskMode(t.Mode), + Model: t.Model, OwnerId: t.OwnerID, ParentTaskId: t.ParentTaskID, ProjectId: t.ProjectID, @@ -122,11 +124,13 @@ func toAttempt(a dbq.TaskAttempt) (api.TaskAttempt, error) { ConfigurationRevision: a.ConfigurationRevision, CreatedAt: a.CreatedAt, Depth: api.TaskAttemptDepth(a.Depth), + Effort: (*api.EffortLevel)(a.Effort), Environment: env, EvidenceRefs: refs, Id: a.ID, InitialRef: a.InitialRef, ManifestRef: a.ManifestRef, + Model: a.Model, NativeDeletedAt: a.NativeDeletedAt, NativeProvider: api.TaskAttemptNativeProvider(a.NativeProvider), Number: int(a.Number), diff --git a/web/src/api/schema.gen.ts b/web/src/api/schema.gen.ts index fae2ad05..f0a970c6 100644 --- a/web/src/api/schema.gen.ts +++ b/web/src/api/schema.gen.ts @@ -2998,6 +2998,16 @@ export interface components { topic_id?: string | null; /** Provider Profile Id */ provider_profile_id?: string | null; + /** + * Model + * @description One of the resolved provider profile's models. Unset keeps the profile's default_model. 422 with the allowed models if the profile does not offer it. + */ + model?: string | null; + /** + * Effort + * @description One of the resolved provider profile's effort_levels. Unset keeps the profile's default_effort. 422 with the allowed levels if the profile does not offer it. + */ + effort?: components['schemas']['EffortLevel'] | null; checkout?: components['schemas']['TaskCheckout'] | null; }; /** TaskReassign */ @@ -3186,6 +3196,10 @@ export interface components { held_tasks?: components['schemas']['TaskCapacityHolder'][]; /** @description parent_capacity only: how to free capacity. */ recovery?: string; + /** @description model_unavailable only: the resolved profile's offered model ids. */ + allowed_models?: string[]; + /** @description effort_unavailable only: the resolved profile's offered effort levels. */ + allowed_effort_levels?: components['schemas']['EffortLevel'][]; } & { [key: string]: unknown; }; @@ -3657,6 +3671,17 @@ export interface components { mode: 'code' | 'coordination'; /** Assigned Profile Id */ assigned_profile_id: string; + /** + * Model + * @description The model the create request chose, validated against assigned_profile_id. Null means the profile's default_model. + * @default null + */ + model: string | null; + /** + * @description The reasoning effort the create request chose, validated against assigned_profile_id. Null means the profile's default_effort. + * @default null + */ + effort: components['schemas']['EffortLevel'] | null; /** * Selection Source * @enum {string} @@ -3756,6 +3781,17 @@ export interface components { native_provider: 'claude' | 'codex'; /** Configuration Revision */ configuration_revision: string; + /** + * Model + * @description The model this attempt started with, or null for the profile's default. + * @default null + */ + model: string | null; + /** + * @description The reasoning effort this attempt started with, or null for the profile's default. + * @default null + */ + effort: components['schemas']['EffortLevel'] | null; agent_ref: components['schemas']['ProviderAgentRef']; /** * Role diff --git a/web/src/features/tasks/TaskDetail.tsx b/web/src/features/tasks/TaskDetail.tsx index 4f73d332..e3e898a1 100644 --- a/web/src/features/tasks/TaskDetail.tsx +++ b/web/src/features/tasks/TaskDetail.tsx @@ -6,6 +6,7 @@ import type { TaskView } from '@/lib/contract'; import { attemptHistory, currentAttempt, + modelEffortLabel, openOperations, providerLabel, publicationFacts, @@ -47,6 +48,7 @@ export function TaskFacts({ const facts = publicationFacts(view.projection); const operations = openOperations(view); const waitingWithoutRequest = view.task.reason === 'approval' && pending.length === 0; + const modelEffort = modelEffortLabel(view); return ( <> @@ -96,6 +98,11 @@ export function TaskFacts({ {providerLabel(view)} {attempt ? ` · attempt ${attempt.number}` : ''}

+ {modelEffort ? ( +

+ {modelEffort} +

+ ) : null}

Status: {statusLabel(view.task.status)} {view.task.reason ? `, ${reasonLabel(view.task.reason)}` : ''} diff --git a/web/src/lib/taskState.test.ts b/web/src/lib/taskState.test.ts index 1fc836a6..02575a7d 100644 --- a/web/src/lib/taskState.test.ts +++ b/web/src/lib/taskState.test.ts @@ -16,6 +16,7 @@ import { ancestors, attemptHistory, buildTree, + modelEffortLabel, normalizeView, observationAge, openOperations, @@ -52,6 +53,23 @@ test('a task with no attempt falls back to its assigned profile', () => { expect(workspaceFacts(queued).workspaceId).toBe(null); }); +test('model and effort are shown only when chosen (#318)', () => { + expect(modelEffortLabel(view())).toBe(null); + const chosenAttempt = view( + {}, + { attempts: [attempt({ model: 'claude-opus-5-5', effort: 'high' })] } + ); + expect(modelEffortLabel(chosenAttempt)).toBe('model: claude-opus-5-5 · effort: high'); + const modelOnly = view({}, { attempts: [attempt({ model: 'claude-opus-5-5' })] }); + expect(modelEffortLabel(modelOnly)).toBe('model: claude-opus-5-5'); + // Before an attempt exists, the task's own chosen model/effort still shows. + const queuedChosen = view( + { status: 'queued', current_attempt_id: null, model: 'claude-opus-5-5', effort: 'high' }, + { attempts: [] } + ); + expect(modelEffortLabel(queuedChosen)).toBe('model: claude-opus-5-5 · effort: high'); +}); + test('observed branch wins over the requested checkout branch', () => { const observed = view( {}, diff --git a/web/src/lib/taskState.ts b/web/src/lib/taskState.ts index 6223ddd0..09c44b45 100644 --- a/web/src/lib/taskState.ts +++ b/web/src/lib/taskState.ts @@ -522,3 +522,19 @@ export function providerLabel(view: TaskView): string { const attempt = currentAttempt(view); return attempt ? attempt.native_provider : view.task.assigned_profile_id; } + +/** + * Model and effort shown alongside the provider (#318): the running attempt's own choice, else + * the task's own (set at delegation, before an attempt exists). Null when neither was ever + * chosen, so the profile's own default applies and there is nothing to show. + */ +export function modelEffortLabel(view: TaskView): string | null { + const attempt = currentAttempt(view); + const model = attempt ? attempt.model : view.task.model; + const effort = attempt ? attempt.effort : view.task.effort; + if (!model && !effort) return null; + const parts: string[] = []; + if (model) parts.push(`model: ${model}`); + if (effort) parts.push(`effort: ${effort}`); + return parts.join(' · '); +} diff --git a/web/src/test/fixtures/tasks.ts b/web/src/test/fixtures/tasks.ts index ff45efe4..1593a641 100644 --- a/web/src/test/fixtures/tasks.ts +++ b/web/src/test/fixtures/tasks.ts @@ -27,6 +27,8 @@ export function task(overrides: Partial = {}): Task { brief: 'Add a retry button to the task page.', mode: 'code', assigned_profile_id: 'claude-default', + model: null, + effort: null, selection_source: 'explicit', provider_constraint: null, accepted_environment: null, @@ -50,6 +52,8 @@ export function attempt(overrides: Partial = {}): TaskAttempt { profile_id: 'claude-default', native_provider: 'claude', configuration_revision: 'rev-1', + model: null, + effort: null, agent_ref: { namespace: 'agents', name: `${overrides.native_provider ?? 'claude'}-child` }, role: 'child', depth: 2,