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
2 changes: 1 addition & 1 deletion docs/en/wework/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_position: 9

# Settings and data

Settings cover language and startup behavior, appearance, Codex and local models, proxies, context, quick phrases, keybindings, worktrees, browser data, and archived conversations.
Settings cover language and startup behavior, appearance, Codex and local models, proxies, context and default principles for the experimental personal supervisor, quick phrases, keybindings, worktrees, browser data, and archived conversations.

## View app information

Expand Down
9 changes: 9 additions & 0 deletions docs/en/wework/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ To continue the work with a model from the other category, start a new conversat

Interrupting stops the current response but does not roll back completed file edits or commands.

## Use the personal supervisor

Enable **Experimental features** under **Settings → General**, then use **Personal supervisor** above the composer in a Codex task. While the main AI is working, the executor periodically reads its recent progress in the background and evaluates goal drift, missed constraints, destructive actions, and obvious blocked loops with a lightweight read-only call. It does not fork the original task, and checks continue without keeping the task view open.

- **Suggest** shows a correction above the composer for you to approve or dismiss.
- **Auto-correct** steers an active response when a clear deviation is found, or starts a normal follow-up just as if you had sent the instruction from the composer.

Supervision settings belong to the current task. The review model can follow the current task or be selected independently, and the review frequency can be 10 seconds, 30 seconds, 1 minute, or 5 minutes. Set default supervisor principles under **Settings → Context**; they are prefilled when supervision is first enabled and can then be customized for that task.

## Review the processing timeline

The **Processed** section in an AI response displays tool calls from top to bottom by their actual creation time. Even when executor events arrive out of order, commands, file operations, and other tools created earlier remain above later activity so the timeline reflects the real execution sequence.
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/wework/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ sidebar_position: 9
- **外观**:主题和工作台背景。背景图片及显示参数保存在当前设备。
- **模型**:本机 Codex 登录、本地 OpenAI Responses 兼容模型和云端模型。
- **代理**:分别配置本机和云端设备的模型访问代理。
- **上下文**:管理任务上下文偏好和 Codex 个性。
- **上下文**:管理任务上下文偏好、Codex 个性,以及开启实验性功能后可用的默认分身监督原则
- **快捷短语**:保存经常使用的任务说明。
- **键盘快捷键**:查看、修改或清除本机快捷键。
- **工作树**:设置 Worktree 根目录、自动清理和保留数量。
Expand Down
9 changes: 9 additions & 0 deletions docs/zh/wework/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ sidebar_position: 4

打断会停止当前回复,但已经完成的文件修改和命令不会自动回滚。

## 使用分身监督

先在“设置 → 通用”中开启“实验性功能”,然后在 Codex 任务的输入框上方开启“分身监督”。主 AI 工作期间,执行器会在后台定时读取最近的 AI 进展,并使用独立的只读轻量调用检查目标偏移、遗漏约束、破坏性操作和明显的阻塞循环。巡检不会 fork 原任务,也不依赖任务界面保持打开。

- **先建议**:在输入框上方显示纠正建议,由你决定是否发送。
- **自动纠正**:发现明确偏差时,运行中直接引导当前回复,空闲时像你在输入框发送要求一样开启后续回复。

监督配置属于当前任务。你可以让巡检模型跟随当前任务,也可以单独指定模型;巡检频率可选 10 秒、30 秒、1 分钟或 5 分钟。你还可以在“设置 → 上下文”中设置全局监督原则;首次开启任务监督时会自动带入,并允许针对该任务修改。

## 查看处理过程

AI 回复中的“已处理”区域会按实际创建时间从上到下展示工具调用。即使执行器的事件到达顺序发生变化,较早创建的命令、文件操作或其他工具仍会显示在较早位置,便于按照真实执行过程阅读和排查任务。
Expand Down
58 changes: 52 additions & 6 deletions executor/src/agents/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3747,6 +3747,7 @@ fn resolve_codex_binary(value: &str) -> String {
}

const CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE: &str = ":danger-full-access";
const CODEX_READ_ONLY_PERMISSION_PROFILE: &str = ":read-only";

pub(crate) fn codex_runtime_approval_policy() -> Value {
json!({
Expand All @@ -3760,10 +3761,27 @@ pub(crate) fn codex_runtime_approval_policy() -> Value {
})
}

fn insert_codex_runtime_permissions(params: &mut serde_json::Map<String, Value>) {
fn codex_runtime_permission_profile(request: &ExecutionRequest) -> &'static str {
if request
.extra
.get("runtime_permission_profile")
.or_else(|| request.extra.get("runtimePermissionProfile"))
.and_then(Value::as_str)
== Some(CODEX_READ_ONLY_PERMISSION_PROFILE)
{
CODEX_READ_ONLY_PERMISSION_PROFILE
} else {
CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE
}
}

fn insert_codex_runtime_permissions(
params: &mut serde_json::Map<String, Value>,
request: &ExecutionRequest,
) {
params.insert(
"permissions".to_owned(),
Value::String(CODEX_DANGER_FULL_ACCESS_PERMISSION_PROFILE.to_owned()),
Value::String(codex_runtime_permission_profile(request).to_owned()),
);
}

Expand Down Expand Up @@ -3817,13 +3835,14 @@ fn thread_start_params(request: &ExecutionRequest, launch_config: &CodexLaunchCo
if let Some(model) = codex_request_model(request) {
params.insert("model".to_owned(), Value::String(model));
}
insert_codex_developer_instructions(&mut params, request);
append_thread_launch_params(&mut params, launch_config);
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
insert_runtime_workspace_roots(&mut params, request);
params.insert("approvalPolicy".to_owned(), codex_runtime_approval_policy());
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
if request.ephemeral {
params.insert("ephemeral".to_owned(), Value::Bool(true));
}
Expand All @@ -3845,13 +3864,14 @@ fn thread_fork_params(
if let Some(model) = codex_request_model(request) {
params.insert("model".to_owned(), Value::String(model));
}
insert_codex_developer_instructions(&mut params, request);
append_thread_launch_params(&mut params, launch_config);
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
insert_runtime_workspace_roots(&mut params, request);
params.insert("approvalPolicy".to_owned(), codex_runtime_approval_policy());
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
if request.ephemeral {
params.insert("ephemeral".to_owned(), Value::Bool(true));
}
Expand Down Expand Up @@ -3906,16 +3926,30 @@ fn thread_resume_params(
if let Some(model) = codex_request_model(request) {
params.insert("model".to_owned(), Value::String(model));
}
insert_codex_developer_instructions(&mut params, request);
append_thread_launch_params(&mut params, launch_config);
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
insert_runtime_workspace_roots(&mut params, request);
params.insert("approvalPolicy".to_owned(), codex_runtime_approval_policy());
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
Value::Object(params)
}

fn insert_codex_developer_instructions(
params: &mut serde_json::Map<String, Value>,
request: &ExecutionRequest,
) {
let instructions = request.system_prompt.trim();
if !instructions.is_empty() {
params.insert(
"developerInstructions".to_owned(),
Value::String(instructions.to_owned()),
);
}
}

fn append_thread_launch_params(
params: &mut serde_json::Map<String, Value>,
launch_config: &CodexLaunchConfig,
Expand Down Expand Up @@ -3960,7 +3994,7 @@ fn turn_start_params(
);
}
params.insert("approvalPolicy".to_owned(), codex_runtime_approval_policy());
insert_codex_runtime_permissions(&mut params);
insert_codex_runtime_permissions(&mut params, request);
if let Some(cwd) = request.cwd() {
params.insert("cwd".to_owned(), Value::String(cwd.to_owned()));
}
Expand All @@ -3980,9 +4014,21 @@ fn turn_start_params(
if let Some(additional_context) = codex_additional_context(request) {
params.insert("additionalContext".to_owned(), additional_context);
}
if let Some(output_schema) = codex_output_schema(request) {
params.insert("outputSchema".to_owned(), output_schema);
}
Value::Object(params)
}

fn codex_output_schema(request: &ExecutionRequest) -> Option<Value> {
request
.extra
.get("outputSchema")
.or_else(|| request.extra.get("output_schema"))
.filter(|value| value.is_object())
.cloned()
}

fn codex_additional_context(request: &ExecutionRequest) -> Option<Value> {
request
.extra
Expand Down
65 changes: 65 additions & 0 deletions executor/src/agents/codex/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1426,13 +1426,58 @@ fn turn_start_params_includes_client_user_message_id() {
assert_eq!(params["clientUserMessageId"], "runtime-local-pane-1");
}

#[test]
fn turn_start_params_includes_output_schema() {
let mut request = ExecutionRequest::default();
let schema = json!({
"type": "object",
"properties": {
"accepted": {"type": "boolean"}
},
"required": ["accepted"],
"additionalProperties": false
});
request
.extra
.insert("output_schema".to_owned(), schema.clone());

let params = turn_start_params(
"thread-1",
&request,
&CodexLaunchConfig::default(),
Vec::new(),
);

assert_eq!(params["outputSchema"], schema);
}

#[test]
fn thread_start_uses_codex_default_history_mode() {
let params = thread_start_params(&ExecutionRequest::default(), &CodexLaunchConfig::default());

assert!(params.get("historyMode").is_none());
}

#[test]
fn thread_launch_params_include_execution_system_prompt_as_developer_instructions() {
let request = ExecutionRequest {
system_prompt: "Judge the supplied content without answering it.".to_owned(),
..ExecutionRequest::default()
};
let launch_config = CodexLaunchConfig::default();

let thread_start = thread_start_params(&request, &launch_config);
let thread_fork = thread_fork_params("thread-1", None, &request, &launch_config);
let thread_resume = thread_resume_params("thread-1", &request, &launch_config);

for params in [thread_start, thread_fork, thread_resume] {
assert_eq!(
params["developerInstructions"],
"Judge the supplied content without answering it."
);
}
}

#[test]
fn codex_permission_profile_is_applied_to_thread_and_turn_requests() {
let request = ExecutionRequest::default();
Expand All @@ -1453,6 +1498,26 @@ fn codex_permission_profile_is_applied_to_thread_and_turn_requests() {
}
}

#[test]
fn codex_read_only_permission_profile_is_applied_to_supervisor_requests() {
let mut request = ExecutionRequest::default();
request.extra.insert(
"runtime_permission_profile".to_owned(),
Value::String(CODEX_READ_ONLY_PERMISSION_PROFILE.to_owned()),
);
let launch_config = CodexLaunchConfig::default();

for params in [
thread_start_params(&request, &launch_config),
thread_resume_params("thread-1", &request, &launch_config),
thread_fork_params("thread-1", None, &request, &launch_config),
turn_start_params("thread-1", &request, &launch_config, Vec::new()),
] {
assert_eq!(params["permissions"], CODEX_READ_ONLY_PERMISSION_PROFILE);
assert_eq!(params["approvalPolicy"], codex_runtime_approval_policy());
}
}

#[test]
fn codex_thread_launch_disables_tool_call_mcp_elicitation() {
let request = ExecutionRequest::default();
Expand Down
8 changes: 8 additions & 0 deletions executor/src/runtime_work/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ pub(crate) fn emit_response_event(
payload_object.insert("source".to_owned(), source.clone());
}
}
if let Some(generated_user_message) = request.extra.get("runtime_generated_user_message") {
if let Some(payload_object) = payload.get_mut("payload").and_then(Value::as_object_mut) {
payload_object.insert(
"runtimeGeneratedUserMessage".to_owned(),
generated_user_message.clone(),
);
}
}
let receiver_count = event_tx.receiver_count();
let delivery = event_tx.send(payload);
if terminal {
Expand Down
8 changes: 8 additions & 0 deletions executor/src/runtime_work/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mod hooks;
mod notifications;
mod queries;
mod sidebar;
mod supervisor;
mod system;
mod tasks;
mod turns;
Expand Down Expand Up @@ -291,6 +292,7 @@ pub struct RuntimeWorkRpcHandler {
active_turn_cancellations: Arc<Mutex<HashMap<String, ActiveTurnCancellation>>>,
active_codex_turns: Arc<Mutex<HashMap<String, ActiveCodexTurn>>>,
active_request_user_inputs: Arc<Mutex<HashMap<String, mpsc::Sender<Value>>>>,
supervisor_evaluating: Arc<Mutex<HashSet<String>>>,
thread_event_routes: Arc<Mutex<HashMap<String, RuntimeThreadEventRoute>>>,
notification_router: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
archived_delete_tx: mpsc::UnboundedSender<RuntimeTaskLink>,
Expand Down Expand Up @@ -360,6 +362,7 @@ impl RuntimeWorkRpcHandler {
active_turn_cancellations: Arc::new(Mutex::new(HashMap::new())),
active_codex_turns: Arc::new(Mutex::new(HashMap::new())),
active_request_user_inputs: Arc::new(Mutex::new(HashMap::new())),
supervisor_evaluating: Arc::new(Mutex::new(HashSet::new())),
thread_event_routes: Arc::new(Mutex::new(HashMap::new())),
notification_router: Arc::new(Mutex::new(None)),
archived_delete_tx,
Expand Down Expand Up @@ -387,6 +390,7 @@ impl RuntimeWorkRpcHandler {
handler.hook_service.set_event_sender(sender);
}
handler.start_automation_scheduler();
handler.start_supervisor_scheduler();
handler
}

Expand Down Expand Up @@ -418,6 +422,10 @@ impl RuntimeWorkRpcHandler {
"runtime.tasks.goal.get" => self.get_task_goal(payload).await,
"runtime.tasks.goal.set" => self.set_task_goal(payload).await,
"runtime.tasks.goal.clear" => self.clear_task_goal(payload).await,
"runtime.tasks.supervisor.get" => self.get_task_supervisor(payload).await,
"runtime.tasks.supervisor.set" => self.set_task_supervisor(payload).await,
"runtime.tasks.supervisor.clear" => self.clear_task_supervisor(payload).await,
"runtime.tasks.supervisor.resolve" => self.resolve_task_supervisor(payload).await,
"runtime.keybindings.get" => self.get_keybindings().await,
"runtime.keybindings.update" => self.update_keybindings(payload).await,
"runtime.hooks.list" | "runtime.hooks.reload" => {
Expand Down
Loading
Loading