Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/engineering/codex_dgx_worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ transcript separately. The current Tasks form's assignee picker offers only
Unassigned and Von; it does not yet offer this worker. The pilot acceptance
used labelled fixtures through canonical services, not that picker.

Native coding tasks can specify `requested_model` (an exact Codex model ID,
for example `gpt-6-astra`) and `requested_reasoning_effort` (for example `high`).
Ask Von to include those fields when creating the task or set them through
`task_update_fields`. Each omitted or cleared field independently inherits the
worker configuration. The worker's built-in defaults are Astra/medium; an
operator can configure a different permitted default. These are execution
preferences on the canonical task, not settings inferred from historical
conversation text. Changes apply at the next launch, not midway through a run.

The launch passes both resolved values explicitly to Codex and retains their
values and sources in `execution_settings` in the run context/checkpoint and
result message. Unsupported selections fail visibly through Codex; the worker
does not substitute another model or reasoning level. The standing Sol block
also applies to task overrides. Reply-only inbox runs use operator defaults
because task selection happens within that reply.

The worker checks at the operator-configured interval, starts at most one coding run per check, and sends a
pickup message followed by a result or question through Von direct messages.
An empty check makes no model request. Each task has its own Git worktree. Each
Expand Down Expand Up @@ -148,7 +164,8 @@ A JSON config has these fields (paths are operator-selected):
"state_root": "/home/mjw/.codex-von-worker/worker-state",
"source_repo": "/home/mjw/von-codex-runtime",
"codex_command": "/home/mjw/.local/bin/von-codex",
"model": "gpt-5.6-terra",
"model": "gpt-6-astra",
"model_reasoning_effort": "medium",
"permission_profile": "von-coding",
"github_command": "/home/mjw/.local/bin/gh",
"git_author_name": "Your configured commit author",
Expand Down
19 changes: 16 additions & 3 deletions scripts/codex_von_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@
from pathlib import Path

try:
from .codex_von_worker import authorised_task, fingerprint, write_json
from .codex_von_worker import (
authorised_task,
fingerprint,
resolve_execution_settings,
write_json,
)
except ImportError:
from codex_von_worker import authorised_task, fingerprint, write_json
from codex_von_worker import (
authorised_task,
fingerprint,
resolve_execution_settings,
write_json,
)

SCHEMA = {
"type": "object",
Expand Down Expand Up @@ -154,12 +164,15 @@ def launch(config, state, lock_fd):
write_json(run / "context.json", state["context"])
prompt = Path(__file__).with_name("codex_von_inbox_prompt.md").read_text()
prompt += "\nContext for this reply:\n" + json.dumps(state["context"], default=str)
settings = resolve_execution_settings(config, {})
args = [
config["codex_command"],
"exec",
"--strict-config",
"--model",
config["model"],
settings["model"],
"-c",
"model_reasoning_effort=" + json.dumps(settings["reasoning_effort"]),
"--sandbox",
"read-only",
"--cd",
Expand Down
58 changes: 54 additions & 4 deletions scripts/codex_von_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,33 @@ def fingerprint(value):
).hexdigest()


def resolve_execution_settings(config, inputs):
"""Resolve task preferences without substituting another requested model."""
resolved = {}
for name, default in (("model", "gpt-6-astra"), ("reasoning_effort", "medium")):
requested = inputs.get("requested_" + name)
configured = config.get(
"model" if name == "model" else "model_reasoning_effort"
)
value = requested if requested is not None else configured
value = default if value is None else value
if (
not isinstance(value, str)
or not value.strip()
or any(c.isspace() for c in value.strip())
):
raise ValueError(
f"Invalid {name}: specify one exact model or reasoning level"
)
resolved[name] = value.strip()
resolved[name + "_source"] = (
"task" if requested is not None else "worker_default"
)
if "sol" in resolved["model"].lower():
raise ValueError("Sol-family models are disabled for this worker")
return resolved


def authorised_task(task, config):
return (
task.get("assignee_concept_id") == config["agent_id"]
Expand Down Expand Up @@ -171,6 +198,8 @@ def inputs(self, task):
return {
"title": task["title"],
"description": task.get("description"),
"requested_model": task.get("requested_model"),
"requested_reasoning_effort": task.get("requested_reasoning_effort"),
"comments": comments,
"replies": [
message for message in replies if not self.inbox_answered(message)
Expand Down Expand Up @@ -320,6 +349,13 @@ def finish(self, state):
f"DGX worktree: {state.get('worktree', 'not started')}\n"
f"Run: {state['attempt']}"
).strip()
if state.get("execution_settings"):
settings = state["execution_settings"]
content += (
f"\nExecution model: {settings['model']} ({settings['model_source']}); "
f"reasoning: {settings['reasoning_effort']} "
f"({settings['reasoning_effort_source']})."
)
# Keep the delivery intent stable across a crash after a task transition.
# A changed/cancelled task is left untouched; the run evidence still goes
# to the original authorised delegator.
Expand Down Expand Up @@ -400,6 +436,12 @@ def launch(config, state, inputs, conversation, state_path, lock_fd):
state.pop("report_task", None)
state.pop("report_content", None)
state.pop("deployment_final", None)
state.pop("execution_settings", None)
write_json(state_path, state)
settings = resolve_execution_settings(config, inputs)
state["execution_settings"] = settings
context["execution_settings"] = settings
write_json(context_path, context)
write_json(state_path, state)
if not state.get("checkout_prepared"):
worktree.parent.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -471,7 +513,9 @@ def launch(config, state, inputs, conversation, state_path, lock_fd):
"exec",
"--strict-config",
"--model",
config["model"],
settings["model"],
"-c",
"model_reasoning_effort=" + json.dumps(settings["reasoning_effort"]),
"--cd",
str(worktree),
"--json",
Expand Down Expand Up @@ -670,14 +714,21 @@ def tick(config, api, lock_fd):
)
try:
launch(config, state, inputs, conversation, path, lock_fd)
except (OSError, subprocess.CalledProcessError) as exc:
except (OSError, subprocess.CalledProcessError, ValueError) as exc:
if "run_dir" not in state:
raise
write_json(
Path(state["run_dir"]) / "launch-error.json",
{"error_type": type(exc).__name__},
)
recover_result(state)
if isinstance(exc, ValueError):
state["result"]["summary"] = (
"Coding execution settings rejected: " + str(exc)
)
state["result"][
"question"
] = "Correct this task's model or reasoning setting, then requeue it."
write_json(path, state)
apply_deployment(config, api, state, path, lock_fd)
api.finish(state)
Expand All @@ -702,8 +753,7 @@ def main():
options = parser.parse_args()
os.umask(0o077)
config = json.loads(Path(options.config).read_text())
if "sol" in config["model"].lower():
raise ValueError("Sol-family models are disabled for this worker")
resolve_execution_settings(config, {})
state_root = Path(config["state_root"])
state_root.mkdir(parents=True, exist_ok=True)
with (state_root / "worker.lock").open("a") as lock:
Expand Down
24 changes: 21 additions & 3 deletions src/backend/integrations/internal_mcp/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -35235,6 +35235,11 @@ def _task_create(**kwargs):
evidence = kwargs.get("evidence")
notes = kwargs.get("notes")
reference_code = kwargs.get("reference_code")
requested_settings = {
key: (value.strip() or None) if isinstance(value, str) else value
for key in ("requested_model", "requested_reasoning_effort")
if (value := kwargs.get(key)) is not None
}
request_id = str(kwargs.get("request_id") or "").strip()
idempotency_key = str(kwargs.get("idempotency_key") or "").strip()

Expand Down Expand Up @@ -35315,6 +35320,7 @@ def _task_create(**kwargs):
"evidence": evidence,
"notes": notes,
"reference_code": reference_code,
**requested_settings,
}
creation_fingerprint = hashlib.sha256(
json.dumps(
Expand All @@ -35330,6 +35336,7 @@ def _task_create(**kwargs):
"description": description.strip(),
"status": "pending",
"priority": priority,
**requested_settings,
}

def _canonical_read_back_mismatches(task: dict[str, Any]) -> dict[str, Any]:
Expand Down Expand Up @@ -35456,6 +35463,7 @@ def _reconcile_after_create_failure() -> dict[str, Any] | None:
evidence=evidence,
notes=notes,
reference_code=reference_code,
**requested_settings,
project_concept_id=kwargs.get("project_concept_id"),
collection_concept_ids=kwargs.get("collection_concept_ids"),
agent_creation_fingerprint=creation_fingerprint,
Expand Down Expand Up @@ -36952,6 +36960,8 @@ def _task_update_fields(**kwargs):
"priority",
"task_type_ids",
"task_role",
"requested_model",
"requested_reasoning_effort",
"next_checkpoint",
"progress_signal",
"evidence",
Expand Down Expand Up @@ -44886,6 +44896,8 @@ def _build_default_catalogue_task_and_workflow_definitions() -> List[MethodDefin
"report_to_concept_id": (str, type(None)),
"reports_to_concept_id": (str, type(None)),
"task_role": (str, type(None)),
"requested_model": (str, type(None)),
"requested_reasoning_effort": (str, type(None)),
"next_checkpoint": (str, type(None)),
"progress_signal": (str, type(None)),
"evidence": (str, type(None)),
Expand Down Expand Up @@ -44934,7 +44946,11 @@ def _build_default_catalogue_task_and_workflow_definitions() -> List[MethodDefin
"assignee_concept_id when reporting responsibility. "
"Priority: low, medium, high, critical. Tasks start in 'pending' status and can include "
"planning metadata (components, fix versions, sprint values, backlog rank), canonical "
"task categories, source semantics, and richer task-detail fields."
"task categories, source semantics, and richer task-detail fields. "
"For coding-agent work, preserve a user-specified model and reasoning level in "
"requested_model (exact model ID, e.g. gpt-6-astra) and "
"requested_reasoning_effort (e.g. medium or high). Omit either to inherit "
"the coding worker default. These fields do not execute the task."
),
),
MethodDefinition(
Expand Down Expand Up @@ -45132,13 +45148,15 @@ def _build_default_catalogue_task_and_workflow_definitions() -> List[MethodDefin
"Update multiple task fields in one operation. "
"Supported fields: status, assignee_concept_id, created_by_concept_id, "
"title, description, priority, task_type_ids, task_source_id, "
"report_to_concept_id, task_role, next_checkpoint, progress_signal, "
"report_to_concept_id, task_role, requested_model, requested_reasoning_effort, "
"next_checkpoint, progress_signal, "
"evidence, notes, reference_code, start_date, due_date, labels, "
"components, fix_versions, sprint_values, backlog_rank, "
"reporter_concept_id, watcher_concept_ids, parent_task_concept_id, "
"epic_task_concept_id, current_work_product_concept_id. "
"For ordinary continuation use title, description, status, priority, "
"task_type_ids, task_role, next_checkpoint, progress_signal, evidence, "
"task_type_ids, task_role, requested_model, requested_reasoning_effort, "
"next_checkpoint, progress_signal, evidence, "
"notes, start_date, due_date, current_work_product_concept_id, "
"assignee_concept_id (self or #V#von_system, creator only), and "
"report_to_concept_id (self)."
Expand Down
2 changes: 2 additions & 0 deletions src/backend/mcp_server/mcp_stdio_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4722,6 +4722,8 @@ async def _handle_create_task(arguments: dict[str, Any]) -> list[TextContent]:
evidence=arguments.get("evidence"),
notes=arguments.get("notes"),
reference_code=arguments.get("reference_code"),
requested_model=arguments.get("requested_model"),
requested_reasoning_effort=arguments.get("requested_reasoning_effort"),
project_concept_id=arguments.get("project_concept_id"),
collection_concept_ids=arguments.get("collection_concept_ids"),
)
Expand Down
8 changes: 8 additions & 0 deletions src/backend/mcp_server/vontology_mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,14 @@
},
"description": "Collections associated with the selected project."
},
"requested_model": {
"type": ["string", "null"],
"description": "Exact coding model requested by the user, e.g. gpt-6-astra. Omit or clear to inherit the worker default."
},
"requested_reasoning_effort": {
"type": ["string", "null"],
"description": "Coding reasoning level requested by the user, e.g. medium or high. Omit or clear to inherit the worker default."
},
"title": {
"type": "string",
"description": "Brief task title (required)"
Expand Down
2 changes: 2 additions & 0 deletions src/backend/server/routes/task_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ def create_task_route() -> ResponseReturnValue:
evidence=data.get("evidence"),
notes=data.get("notes"),
reference_code=data.get("reference_code"),
requested_model=data.get("requested_model"),
requested_reasoning_effort=data.get("requested_reasoning_effort"),
project_concept_id=data.get("project_concept_id"),
collection_concept_ids=data.get("collection_concept_ids"),
)
Expand Down
30 changes: 30 additions & 0 deletions src/backend/services/task_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,8 @@ def create_task(
evidence: str | None = None,
notes: str | None = None,
reference_code: str | None = None,
requested_model: str | None = None,
requested_reasoning_effort: str | None = None,
agent_creation_fingerprint: str | None = None,
agent_creation_request_id: str | None = None,
project_concept_id: str | None = None,
Expand Down Expand Up @@ -1396,6 +1398,12 @@ def create_task(
reference_code,
field_name="reference_code",
)
requested_model = _normalise_optional_text(
requested_model, field_name="requested_model"
)
requested_reasoning_effort = _normalise_optional_text(
requested_reasoning_effort, field_name="requested_reasoning_effort"
)
agent_creation_fingerprint = _normalise_optional_text(
agent_creation_fingerprint,
field_name="agent_creation_fingerprint",
Expand Down Expand Up @@ -1537,6 +1545,8 @@ def create_task(
TASK_METADATA_KEY_BULK_TASK_COLLECTIONS: [],
TASK_METADATA_KEY_CREATION_FINGERPRINT: agent_creation_fingerprint,
TASK_METADATA_KEY_CREATION_REQUEST_ID: agent_creation_request_id,
"requested_model": requested_model,
"requested_reasoning_effort": requested_reasoning_effort,
"project_concept_id": project_concept_id,
"collection_concept_ids": collection_concept_ids or [],
},
Expand Down Expand Up @@ -1657,6 +1667,8 @@ def create_task(
"evidence": evidence,
"notes": notes,
"reference_code": reference_code,
"requested_model": requested_model,
"requested_reasoning_effort": requested_reasoning_effort,
"created_at": now.isoformat(),
"required_text_persistence_failures": required_text_persistence_failures,
}
Expand Down Expand Up @@ -2163,6 +2175,8 @@ def _build_task_response(
"evidence": evidence,
"notes": notes,
"reference_code": reference_code,
"requested_model": metadata.get("requested_model"),
"requested_reasoning_effort": metadata.get("requested_reasoning_effort"),
"parent_task_concept_id": parent_task_id,
"epic_task_concept_id": epic_task_id,
"subtask_concept_ids": subtask_ids,
Expand Down Expand Up @@ -4744,6 +4758,12 @@ def update_task_fields(
raise InvalidTaskDataError("fields must be a non-empty dict")

existing_task = _build_task_response(task_doc)
# Validate these settings before changing any of the requested task fields.
execution_settings = {
key: _normalise_optional_text(fields[key], field_name=key)
for key in ("requested_model", "requested_reasoning_effort")
if key in fields
}
membership = None
if "project_concept_id" in fields or "collection_concept_ids" in fields:
from .task_project_service import validate_task_membership
Expand Down Expand Up @@ -4860,6 +4880,14 @@ def update_task_fields(
if next_start is not None and next_due is not None and next_start > next_due:
raise InvalidTaskDataError("start_date must be before or equal to due_date")

for key, value in execution_settings.items():
if existing_task.get(key) != value:
ConceptsRepository.update_one(
{"concept_id": task_concept_id},
{"$set": {f"metadata.{key}": value}},
)
changed_fields.append(key)

if product_field_present:
_replace_single_relationship_target(
task_concept_id=task_concept_id,
Expand Down Expand Up @@ -5360,6 +5388,8 @@ def update_task_fields(
"evidence",
"notes",
"reference_code",
"requested_model",
"requested_reasoning_effort",
"start_date",
"due_date",
"labels",
Expand Down
1 change: 1 addition & 0 deletions tests/backend/test_codex_von_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def test_inbox_launch_is_read_only_and_inherits_the_single_worker_lock(
def run(args, **kwargs):
assert args[args.index("--sandbox") + 1] == "read-only"
assert kwargs["pass_fds"] == (17,)
assert 'model_reasoning_effort="medium"' in args
assert not set(kwargs["env"]) & {"OPENAI_API_KEY", "MONGO_URI"}
output = inbox.Path(args[args.index("--output-last-message") + 1])
inbox.write_json(
Expand Down
Loading
Loading