You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/22 17:01:26 UTC

(maka) branch feat/runtime-host-rust updated: feat(workhub): repair delegated models through public plugin capabilities

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
     new ef3e081b2a feat(workhub): repair delegated models through public plugin capabilities
ef3e081b2a is described below

commit ef3e081b2a2e9eca4a70abec578cf1ab5295012f
Author: Wang <m4...@gmail.com>
AuthorDate: Wed Sep 23 01:01:18 2026 +0800

    feat(workhub): repair delegated models through public plugin capabilities
    
    Generated-by: Codex
---
 .../tests/integration/workhub_public.rs            | 112 +++++++++++-
 crates/workhub/src/assignment.rs                   |  18 +-
 crates/workhub/src/lib.rs                          |   1 +
 crates/workhub/src/model_repair.rs                 | 192 +++++++++++++++++++++
 crates/workhub/src/observation.rs                  |   7 +
 crates/workhub/src/plugin/remote.rs                |  68 ++++++++
 docs/rust-runtime.md                               |   3 +
 docs/rust-runtime.zh-CN.md                         |   2 +
 packages/workhub/src/client.tsx                    |  12 +-
 packages/workhub/src/delegation-models.tsx         | 192 +++++++++++++++++++++
 packages/workhub/src/model-selection.tsx           |  15 +-
 11 files changed, 599 insertions(+), 23 deletions(-)

diff --git a/crates/runtime-host/tests/integration/workhub_public.rs b/crates/runtime-host/tests/integration/workhub_public.rs
index 8895992bec..2287f2f6a6 100644
--- a/crates/runtime-host/tests/integration/workhub_public.rs
+++ b/crates/runtime-host/tests/integration/workhub_public.rs
@@ -99,6 +99,8 @@ async fn scenario() {
     let mut intent = Value::Null;
     let mut coordinator = String::new();
     let mut controls = Vec::<Value>::new();
+    let mut repair_intent = Value::Null;
+    let mut repaired = Value::Null;
     for reopened in [false, true] {
         let host = Host::open_with_options(
             fixture.owner(),
@@ -215,7 +217,13 @@ async fn scenario() {
             assert_eq!(view["behavior"], "z.workhub.coordinator");
             assert_eq!(view["sandboxMode"], "workspace-write");
             assert_eq!(view["approvalPolicy"], json!({"kind":"on-request"}));
-            restricted_selection(address, &client, json!(model)).await;
+            restricted_selection(
+                address,
+                &client,
+                "select-coordinator-model",
+                json!({"kind":"model","model":model,"thinkingLevel":null}),
+            )
+            .await;
             success(peer.rpc("session.create", json!({
                 "sessionId":"workhub-target", "workspace":{"kind":"host_path","path":fixture.workspace},
                 "modelTarget":{"kind":"explicit","connectionId":model.connection_id,"connectionSlug":model.connection_slug,"model":model.model}
@@ -630,15 +638,102 @@ async fn scenario() {
         )
         .await;
         assert_eq!(stopped["kind"], "stopped");
+        if !reopened {
+            repair_intent = json!({
+                "operationId":"repair-root", "source":intent["source"],
+                "authorization":{"kind":"plugin_workspace","sandboxMode":"workspace-write"},
+                "target":{"kind":"create","request":{
+                    "operationId":"repairable-root", "managed":false, "name":"Repairable delegated work",
+                    "settings":{
+                        "target":{"kind":"model","model":model,"thinkingLevel":"max"},
+                        "sandboxMode":"read-only", "approvalPolicy":{"kind":"on-request"},
+                        "collaborationMode":"agent", "behavior":"default"
+                    }
+                }},
+                "content":{"text":"Only execute the repaired model"}
+            });
+            let failed = remote_result(
+                &mut peer,
+                &client,
+                &document,
+                Some(&coordinator),
+                "route",
+                repair_intent.clone(),
+            )
+            .await;
+            assert_eq!(
+                failed["ok"], false,
+                "unsupported thinking must not admit work"
+            );
+            let before = remote(
+                &mut peer,
+                &client,
+                &document,
+                None,
+                "delegation-model",
+                json!({"assignmentId":"repair-root"}),
+            )
+            .await;
+            assert_eq!(before["revision"], Value::Null);
+            let choice = json!({"assignmentId":"repair-root", "expectedRevision":before["revision"],
+                "target":{"kind":"model","model":model,"thinkingLevel":null}});
+            restricted_selection(address, &client, "select-delegation-model", choice.clone()).await;
+            let repaired_session = remote(
+                &mut peer,
+                &client,
+                &document,
+                None,
+                "select-delegation-model",
+                choice.clone(),
+            )
+            .await;
+            assert_eq!(repaired_session["target"]["thinkingLevel"], Value::Null);
+            let stale = remote_result(
+                &mut peer,
+                &client,
+                &document,
+                None,
+                "select-delegation-model",
+                choice,
+            )
+            .await;
+            assert_eq!(
+                stale["ok"], false,
+                "stale model choices must not overwrite the repair"
+            );
+            repaired = remote(
+                &mut peer,
+                &client,
+                &document,
+                Some(&coordinator),
+                "route",
+                repair_intent.clone(),
+            )
+            .await;
+            wait_assignment(&mut peer, &client, &document, "repair-root").await;
+        } else {
+            assert_eq!(
+                remote(
+                    &mut peer,
+                    &client,
+                    &document,
+                    Some(&coordinator),
+                    "route",
+                    repair_intent.clone()
+                )
+                .await,
+                repaired
+            );
+        }
         peer.close().await;
         stop.cancel();
         server.await.unwrap().unwrap();
         cleanup.disarm();
         drop(host);
     }
-    // Seven original steps, two shared-work and three selection steps; no replayed admission.
+    // Original, shared/selected and repaired work; no replayed admission after restart.
     let requests = provider.requests.lock().unwrap();
-    assert_eq!(requests.len(), 12);
+    assert_eq!(requests.len(), 13);
     let advertised = |request: &Value| {
         request["tools"].as_array().is_some_and(|tools| {
             tools
@@ -737,7 +832,12 @@ async fn upload(peer: &mut Peer, session: &str) -> Value {
         .clone()
 }
 
-async fn restricted_selection(address: std::net::SocketAddr, client: &Value, model: Value) {
+async fn restricted_selection(
+    address: std::net::SocketAddr,
+    client: &Value,
+    method: &str,
+    selection: Value,
+) {
     use futures_util::{SinkExt, StreamExt};
     use tokio_tungstenite::tungstenite::{Message, client::IntoClientRequest};
     let mut request = format!("ws://{address}/runtime-host")
@@ -762,13 +862,13 @@ async fn restricted_selection(address: std::net::SocketAddr, client: &Value, mod
     assert_eq!(hello["state"], "ready");
     let mut document = Value::Null;
     let mut target = Value::Null;
-    let binding = json!({"client":client,"method":"select-coordinator-model","sessionId":null});
+    let binding = json!({"client":client,"method":method,"sessionId":null});
     for step in ["document", "bind", "call"] {
         let input = match step {
             "document" => json!({"kind":"open_document"}),
             "bind" => json!({"kind":"bind","binding":binding}),
             _ => json!({"kind":"call","binding":binding,"document":document,"target":target,
-                "input":{"kind":"model","model":model,"thinkingLevel":null}}),
+                "input":selection}),
         };
         socket
             .send(Message::text(
diff --git a/crates/workhub/src/assignment.rs b/crates/workhub/src/assignment.rs
index 97eec85b8b..3c4e6b6e1c 100644
--- a/crates/workhub/src/assignment.rs
+++ b/crates/workhub/src/assignment.rs
@@ -92,18 +92,6 @@ impl Assignment {
     }
 }
 
-async fn restore_or_create_root(
-    commands: &dyn Commands,
-    request: &CreateRoot,
-) -> Result<String, Error> {
-    Ok(
-        match commands.restore_root(request.operation_id.clone()).await? {
-            Some(session) => session.session_id,
-            None => commands.create_root(request.clone()).await?.session_id,
-        },
-    )
-}
-
 pub struct Assignments {
     pub coordinator: Arc<crate::Coordinator>,
     pub repository: Arc<Repository>,
@@ -162,7 +150,8 @@ impl Assignments {
                     let session = match &assignment.request.target {
                         Target::Existing { session_id } => session_id.clone(),
                         Target::Create { request } => {
-                            restore_or_create_root(commands.as_ref(), request).await?
+                            self.restore_or_create_root(commands.as_ref(), request)
+                                .await?
                         }
                     };
                     let mut content = assignment.request.content.clone();
@@ -261,7 +250,8 @@ impl Assignments {
         match &assignment.request.target {
             Target::Create { request } => {
                 // Reattach exactly the root this grant created, including after a restart.
-                restore_or_create_root(commands.as_ref(), request).await?;
+                self.restore_or_create_root(commands.as_ref(), request)
+                    .await?;
             }
             Target::Existing { session_id } => {
                 commands.session(session_id.clone()).await?;
diff --git a/crates/workhub/src/lib.rs b/crates/workhub/src/lib.rs
index f88d3366fd..bf9f2ccb1d 100644
--- a/crates/workhub/src/lib.rs
+++ b/crates/workhub/src/lib.rs
@@ -25,6 +25,7 @@ mod control;
 mod coordinator;
 mod decision;
 mod feedback;
+mod model_repair;
 mod observation;
 mod plugin;
 mod queue;
diff --git a/crates/workhub/src/model_repair.rs b/crates/workhub/src/model_repair.rs
new file mode 100644
index 0000000000..99985191ba
--- /dev/null
+++ b/crates/workhub/src/model_repair.rs
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::{
+    Error,
+    assignment::{Assignment, Assignments, Target},
+    invalid,
+    repository::digest,
+};
+use maka_plugins::{
+    authorization,
+    execution::{
+        CommandError, Commands, Configure, Configured, CreateRoot, Target as ExecutionTarget,
+    },
+    llm::Selection,
+    session::View,
+};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(super) struct Choice {
+    pub revision: Option<u64>,
+    pub authorization: authorization::Target,
+    pub name: String,
+    pub target: Option<ExecutionTarget>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Selected {
+    id: uuid::Uuid,
+    target: ExecutionTarget,
+}
+
+impl Assignments {
+    pub(super) async fn model_choice(&self, operation: &str) -> Result<Choice, Error> {
+        let assignment = self.model_assignment(operation).await?;
+        let choice = self
+            .repository
+            .read::<Selected>(&choice_key(&assignment.request.target)?)
+            .await?;
+        let (name, original) = match &assignment.request.target {
+            Target::Existing { session_id } => (session_id.clone(), None),
+            Target::Create { request } => {
+                (request.name.clone(), Some(request.settings.target.clone()))
+            }
+        };
+        Ok(Choice {
+            revision: choice.as_ref().map(|(revision, _)| *revision),
+            authorization: assignment.request.authorization,
+            name,
+            target: choice.map(|(_, choice)| choice.target).or(original),
+        })
+    }
+
+    async fn model_assignment(&self, operation: &str) -> Result<Assignment, Error> {
+        let assignment = self
+            .repository
+            .read::<Assignment>(&crate::assignment::key(operation)?)
+            .await?
+            .ok_or(Error::Conflict)?
+            .1;
+        if assignment.retired {
+            return Err(Error::Conflict);
+        }
+        Ok(assignment)
+    }
+
+    pub(super) async fn select_model(
+        &self,
+        operation: &str,
+        expected_revision: Option<u64>,
+        target: ExecutionTarget,
+        commands: &dyn Commands,
+    ) -> Result<View, Error> {
+        let assignment = self.model_assignment(operation).await?;
+        let ExecutionTarget::Model {
+            model,
+            thinking_level,
+        } = &target
+        else {
+            return Err(invalid("Select a replacement model"));
+        };
+        target.validate().map_err(invalid)?;
+        let choice = self
+            .coordinator
+            .models
+            .resolve(Selection::Named {
+                connection_slug: model.connection_slug.clone(),
+                model: model.model.clone(),
+            })
+            .await
+            .map_err(invalid)?
+            .ok_or_else(|| invalid("Selected model is unavailable"))?;
+        if choice.model != *model
+            || thinking_level.is_some_and(|level| !choice.thinking_levels.contains(&level))
+        {
+            return Err(invalid(
+                "Selected model identity or thinking level is unavailable",
+            ));
+        }
+        commands.validate_authority().await?;
+        let key = choice_key(&assignment.request.target)?;
+        let selected = Selected {
+            id: uuid::Uuid::new_v4(),
+            target,
+        };
+        // A choice never rewrites the frozen Route or execution operation.
+        self.repository
+            .put(&key, expected_revision, &selected)
+            .await?;
+        let session_id = match &assignment.request.target {
+            Target::Existing { session_id } => session_id.clone(),
+            Target::Create { request } => self.restore_or_create_root(commands, request).await?,
+        };
+        let session = commands.session(session_id).await?;
+        let current = self
+            .repository
+            .read::<Selected>(&key)
+            .await?
+            .ok_or(Error::Conflict)?;
+        if current.1.id != selected.id {
+            return Err(Error::Contended);
+        }
+        match commands
+            .configure(Configure {
+                session_id: session.session_id,
+                expected_revision: session.revision,
+                target: selected.target,
+            })
+            .await?
+        {
+            Configured::Committed { session } => Ok(*session),
+            Configured::RevisionConflict { .. } => Err(Error::Contended),
+        }
+    }
+
+    pub(super) async fn restore_or_create_root(
+        &self,
+        commands: &dyn Commands,
+        request: &CreateRoot,
+    ) -> Result<String, Error> {
+        if let Some(session) = commands.restore_root(request.operation_id.clone()).await? {
+            return Ok(session.session_id);
+        }
+        let mut proposal = request.clone();
+        if let Some((_, choice)) = self
+            .repository
+            .read::<Selected>(&root_key(&request.operation_id)?)
+            .await?
+        {
+            proposal.settings.target = choice.target;
+        }
+        match commands.create_root(proposal).await {
+            Ok(session) => Ok(session.session_id),
+            Err(CommandError::Conflict) => commands
+                .restore_root(request.operation_id.clone())
+                .await?
+                .map(|session| session.session_id)
+                .ok_or(Error::Conflict),
+            Err(error) => Err(error.into()),
+        }
+    }
+}
+
+fn choice_key(target: &Target) -> Result<String, Error> {
+    match target {
+        Target::Create { request } => root_key(&request.operation_id),
+        Target::Existing { session_id } => {
+            Ok(format!("model-choices/session/{}", digest(session_id)?))
+        }
+    }
+}
+fn root_key(operation: &str) -> Result<String, Error> {
+    Ok(format!("model-choices/root/{}", digest(&operation)?))
+}
diff --git a/crates/workhub/src/observation.rs b/crates/workhub/src/observation.rs
index 29d4736708..725f7c144c 100644
--- a/crates/workhub/src/observation.rs
+++ b/crates/workhub/src/observation.rs
@@ -52,6 +52,7 @@ pub struct Page {
 #[serde(rename_all = "camelCase")]
 pub struct Summary {
     pub operation_id: String,
+    pub title: String,
     pub source: Invocation,
     pub delivery: Option<Delivery>,
     pub retired: bool,
@@ -67,6 +68,12 @@ impl Assignments {
             entries: assignments
                 .into_iter()
                 .map(|assignment| Summary {
+                    title: match &assignment.request.target {
+                        crate::assignment::Target::Create { request } => request.name.clone(),
+                        crate::assignment::Target::Existing { .. } => {
+                            assignment.request.content.text.chars().take(160).collect()
+                        }
+                    },
                     operation_id: assignment.request.operation_id,
                     source: assignment.request.source,
                     delivery: assignment.delivery,
diff --git a/crates/workhub/src/plugin/remote.rs b/crates/workhub/src/plugin/remote.rs
index 4dc89a6bab..7ed6a7c8c2 100644
--- a/crates/workhub/src/plugin/remote.rs
+++ b/crates/workhub/src/plugin/remote.rs
@@ -45,6 +45,8 @@ pub(super) fn publish(
         ("models", Action::Models),
         ("executors", Action::Executors),
         ("select-coordinator-model", Action::SelectModel),
+        ("delegation-model", Action::DelegationModel),
+        ("select-delegation-model", Action::SelectDelegationModel),
         ("query", Action::Query),
         ("candidates", Action::Candidates),
         ("configure-creation", Action::Creation),
@@ -84,6 +86,8 @@ enum Action {
     Models,
     Executors,
     SelectModel,
+    DelegationModel,
+    SelectDelegationModel,
     Query,
     Candidates,
     Creation,
@@ -231,6 +235,70 @@ impl Method for Call {
                         json!({"coordinatorSessionId":manager.coordinator.session_id().await.map_err(failure)?, "recovery":manager.report.lock().unwrap().clone()}),
                     )
                 }
+                Action::DelegationModel => {
+                    #[derive(Deserialize)]
+                    #[serde(rename_all = "camelCase", deny_unknown_fields)]
+                    struct Input {
+                        assignment_id: String,
+                    }
+                    let input: Input = decode(input)?;
+                    encode(
+                        manager
+                            .assignments
+                            .model_choice(&input.assignment_id)
+                            .await
+                            .map_err(failure)?,
+                    )
+                }
+                Action::SelectDelegationModel => {
+                    #[derive(Deserialize)]
+                    #[serde(rename_all = "camelCase", deny_unknown_fields)]
+                    struct Input {
+                        assignment_id: String,
+                        expected_revision: Option<u64>,
+                        target: maka_plugins::execution::Target,
+                    }
+                    let input: Input = decode(input)?;
+                    let choice = manager
+                        .assignments
+                        .model_choice(&input.assignment_id)
+                        .await
+                        .map_err(failure)?;
+                    let authority = caller
+                        .views
+                        .authorize(Authorization {
+                            operation_id: uuid::Uuid::new_v4(),
+                            title: "Choose the delegated task model".into(),
+                            target: choice.authorization,
+                            capabilities: [Capability::Executions].into(),
+                        })
+                        .await?;
+                    let result = async {
+                        let commands = manager
+                            .executions
+                            .acquire(authority.scope())
+                            .await
+                            .map_err(command)?;
+                        encode(
+                            manager
+                                .assignments
+                                .select_model(
+                                    &input.assignment_id,
+                                    input.expected_revision,
+                                    input.target,
+                                    commands.as_ref(),
+                                )
+                                .await
+                                .map_err(failure)?,
+                        )
+                    }
+                    .await;
+                    authority
+                        .finish()
+                        .await
+                        .map_err(|_| Error::CleanupUnconfirmed)?;
+                    result
+                }
                 Action::Candidates => {
                     empty(input)?;
                     encode(manager.candidates().await.map_err(failure)?)
diff --git a/docs/rust-runtime.md b/docs/rust-runtime.md
index 858e44a3f2..e08d4e29df 100644
--- a/docs/rust-runtime.md
+++ b/docs/rust-runtime.md
@@ -428,6 +428,9 @@ live verification are deferred.
 
 WorkHub supports scoped conversation, candidate discovery, interactive target selection,
 delegation to existing or new sessions, steering, stop, resume and correction.
+Its delegated-model panel can repair a failed creation or change an existing target's model.
+Selections use current caller authorization and configuration CAS; the original delegation,
+root identity and execution receipts remain unchanged. Retry after a concurrent or busy response.
 Control actions follow the exact delegated Message, never an unrelated Run.
 Correction records intent before retirement, then atomically commits the replacement,
 attachments and queued delivery. Recovery can finish after the coordinator Run ends;
diff --git a/docs/rust-runtime.zh-CN.md b/docs/rust-runtime.zh-CN.md
index 25974c18dd..5185d59748 100644
--- a/docs/rust-runtime.zh-CN.md
+++ b/docs/rust-runtime.zh-CN.md
@@ -338,6 +338,8 @@ Grep 差分测试需要 PATH 中有 `rg`;runtime 本身不依赖该可执行
 模型流式交互与上下文压缩。Codex 订阅已接入执行;Copilot/xAI 推理适配和实测后置。
 
 WorkHub 已支持受限对话、候选发现、交互式目标选择、向已有或新建会话委派、steering、停止、恢复和纠正。
+“委派模型与恢复”可修复创建失败的任务,也可更换已有目标的模型;使用当前调用者授权及配置 CAS,
+保留原委派、根会话身份与执行回执。遇到并发修改或任务忙碌时,可刷新后重试。
 控制操作追踪精确的委派 Message,不操作无关 Run。纠正先持久化意图,再退休旧关联,
 原子提交替换、附件与排队消息;协调 Run 结束后仍可恢复完成,新目标不可用时持久化中止结果,
 不会取消共享 Run。结构化记录保留原始创建选项与附件归属,候选查询提供最新有效关联。
diff --git a/packages/workhub/src/client.tsx b/packages/workhub/src/client.tsx
index 83beba2e71..3cfce83090 100644
--- a/packages/workhub/src/client.tsx
+++ b/packages/workhub/src/client.tsx
@@ -26,6 +26,7 @@ import { WorkHubRoot } from './surface.js';
 import { coordinationCommands } from './client-session.js';
 import { bindSurface } from './client-surface.js';
 import { ModelSelection, type ModelTarget } from './model-selection.js';
+import { DelegationModels } from './delegation-models.js';
 import styles from './styles.css';
 
 type Resolution = { sessionId: string };
@@ -52,7 +53,16 @@ const plugin: ClientPlugin = {
           props.hostSessionId,
         ],
       );
-      return <WorkHubRoot {...props} {...bound} signal={context.signal} />;
+      return (
+        <>
+          <DelegationModels
+            context={context}
+            locale={props.locale}
+            contextRevision={props.contextRevision}
+          />
+          <WorkHubRoot {...props} {...bound} signal={context.signal} />
+        </>
+      );
     });
     const resolve = context.remote.method<null, Resolution>('resolve');
     context.slots.register(
diff --git a/packages/workhub/src/delegation-models.tsx b/packages/workhub/src/delegation-models.tsx
new file mode 100644
index 0000000000..ed4061e2c5
--- /dev/null
+++ b/packages/workhub/src/delegation-models.tsx
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useState } from 'react';
+import { Button } from '@maka/ui/plugin';
+import type { AuthorizationRequest, ClientContext } from '@maka-agent/plugin-sdk/client';
+import type { ExecutionTarget } from '@maka-agent/plugin-sdk/host';
+import { authorize } from './access.js';
+import { ModelSelection } from './model-selection.js';
+
+type Page = {
+  entries: { operationId: string; title: string; retired: boolean }[];
+  nextAfter: string | null;
+};
+type Choice = {
+  revision: number | null;
+  authorization: AuthorizationRequest['target'];
+  name: string;
+  target: ExecutionTarget | null;
+};
+type Status = {
+  recovery: { unavailable?: string; failures: { operationId: string; message: string }[] };
+};
+
+export function DelegationModels({
+  context,
+  locale,
+  contextRevision,
+}: {
+  context: ClientContext;
+  locale: string;
+  contextRevision?: number;
+}) {
+  const zh = locale !== 'en';
+  const [open, setOpen] = useState(false);
+  const [after, setAfter] = useState<string>();
+  const [page, setPage] = useState<Page>();
+  const [status, setStatus] = useState<Status>();
+  const [selected, setSelected] = useState('');
+  const [choice, setChoice] = useState<{ operation: string; value: Choice }>();
+  const [error, setError] = useState<string>();
+  const [busy, setBusy] = useState(false);
+  const [refresh, setRefresh] = useState(0);
+  useEffect(() => {
+    let active = true;
+    if (open)
+      void Promise.all([
+        context.remote.method<{ after?: string }, Page>('assignments')({ after }),
+        context.remote.method<null, Status>('query')(null),
+      ])
+        .then(([page, status]) => {
+          if (active) {
+            setPage(page);
+            setStatus(status);
+          }
+        })
+        .catch((error: unknown) => {
+          if (active) setError(error instanceof Error ? error.message : String(error));
+        });
+    return () => {
+      active = false;
+    };
+  }, [context, open, after, refresh, contextRevision]);
+  const operation =
+    open && page?.entries.some((entry) => entry.operationId === selected && !entry.retired)
+      ? selected
+      : undefined;
+  useEffect(() => {
+    let active = true;
+    setChoice((current) => (current?.operation === operation ? current : undefined));
+    if (operation)
+      void context.remote
+        .method<{ assignmentId: string }, Choice>('delegation-model')({ assignmentId: operation })
+        .then((value) => {
+          if (active) setChoice({ operation, value });
+        })
+        .catch((error: unknown) => {
+          if (active) setError(error instanceof Error ? error.message : String(error));
+        });
+    return () => {
+      active = false;
+    };
+  }, [context, operation, refresh]);
+  const current = choice?.operation === operation ? choice : undefined;
+  return (
+    <details open={open} onToggle={(event) => setOpen(event.currentTarget.open)}>
+      <summary>{zh ? '委派模型与恢复' : 'Delegated models and recovery'}</summary>
+      {open ? (
+        <div className="workhub-model-selection">
+          {status?.recovery.unavailable ? <p role="alert">{status.recovery.unavailable}</p> : null}
+          {status?.recovery.failures.map((failure) => (
+            <p role="alert" key={failure.operationId}>
+              {failure.message}
+            </p>
+          ))}
+          <label>
+            {zh ? '委派任务' : 'Delegated task'}
+            <select
+              disabled={busy}
+              value={operation ?? ''}
+              onChange={(event) => setSelected(event.target.value)}
+            >
+              <option value="">{zh ? '选择委派任务' : 'Choose a delegated task'}</option>
+              {page?.entries
+                .filter((entry) => !entry.retired)
+                .map((entry) => (
+                  <option key={entry.operationId} value={entry.operationId}>
+                    {entry.title}
+                  </option>
+                ))}
+            </select>
+          </label>
+          <Button
+            label={zh ? '刷新' : 'Refresh'}
+            isDisabled={busy}
+            onClick={() => setRefresh((value) => value + 1)}
+          />
+          {after ? (
+            <Button
+              isDisabled={busy}
+              label={zh ? '首页' : 'First page'}
+              onClick={() => {
+                setSelected('');
+                setAfter(undefined);
+              }}
+            />
+          ) : null}
+          {page?.nextAfter ? (
+            <Button
+              isDisabled={busy}
+              label={zh ? '下一页' : 'Next page'}
+              onClick={() => {
+                setSelected('');
+                setAfter(page.nextAfter!);
+              }}
+            />
+          ) : null}
+          {error ? <p role="alert">{error}</p> : null}
+          {current ? (
+            <>
+              <p>{current.value.name}</p>
+              <ModelSelection
+                key={current.operation}
+                context={context}
+                locale={locale}
+                initialTarget={
+                  current.value.target?.kind === 'model' ? current.value.target : undefined
+                }
+                label={zh ? '使用此模型恢复委派' : 'Use this model for the delegated task'}
+                onSelect={async (target) => {
+                  setBusy(true);
+                  setError(undefined);
+                  try {
+                    await authorize(
+                      context,
+                      current.value.authorization,
+                      zh ? '更换委派任务模型' : 'Change the delegated task model',
+                    );
+                    await context.remote.method('select-delegation-model')({
+                      assignmentId: current.operation,
+                      expectedRevision: current.value.revision,
+                      target,
+                    });
+                  } finally {
+                    setBusy(false);
+                    setRefresh((value) => value + 1);
+                  }
+                }}
+              />
+            </>
+          ) : null}
+        </div>
+      ) : null}
+    </details>
+  );
+}
diff --git a/packages/workhub/src/model-selection.tsx b/packages/workhub/src/model-selection.tsx
index e06f135df7..0c48ec23c4 100644
--- a/packages/workhub/src/model-selection.tsx
+++ b/packages/workhub/src/model-selection.tsx
@@ -29,17 +29,28 @@ export function ModelSelection({
   locale,
   label,
   onSelect,
+  initialTarget,
 }: {
   context: ClientContext;
   locale: string;
   label: string;
   onSelect: (target: ModelTarget) => Promise<void>;
+  initialTarget?: ModelTarget;
 }) {
   const zh = locale !== 'en';
   const [query, setQuery] = useState('');
   const [choices, setChoices] = useState<ModelChoices>();
-  const [selected, setSelected] = useState('');
-  const [thinking, setThinking] = useState<{ model: string; level: string }>();
+  const [selected, setSelected] = useState(() =>
+    initialTarget ? JSON.stringify(initialTarget.model) : '',
+  );
+  const [thinking, setThinking] = useState<{ model: string; level: string } | undefined>(() =>
+    initialTarget
+      ? {
+          model: JSON.stringify(initialTarget.model),
+          level: initialTarget.thinkingLevel ?? '',
+        }
+      : undefined,
+  );
   const [busy, setBusy] = useState(false);
   const [error, setError] = useState<string>();
   useEffect(() => {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org