mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(managed-agent): link a child task to its child Session (#13746 F2) A child_agent or workflow task now names the child Session that runs it: PublicTask.child_session_id and WebShellTask.childSessionId, present once the run's committed child_run record names the child (its attach, or a terminal verdict that names a minted child) and absent for every other kind. The child is a plain Session in the parent's Workspace, so the callers that can read the task already read the child's Turns, items and events; the link adds no authorization rule, route or event type. The task view reads the id from the record's committed body rather than a new projected column, so there is no migration or backfill. The cancel admission's task lookup stays a pure projection read. Contract 1.54.0 (provisional) adds the optional field with a kind conditional on both task schemas; the WebShell types are regenerated. * docs(managed-agent): note the child Session link contract number against main
This commit is contained in:
1 parent
f597df5a60
commit
809792a198
18 files changed
+329
-16
No files matched your search
@@ -91,6 +91,7 @@ resources:
|
||||
| `taskId` | `id` | `taskId` | As `PublicSession` and `PublicAction`. |
|
||||
| (none) | `object` | (none) | Added, `agent.task`. |
|
||||
| `sessionId` | `session_id` | `sessionId` | |
|
||||
| (none) | `child_session_id` | `childSessionId` | Child kinds only (F2 of #13746). |
|
||||
| `kind` | `kind` | `kind` | `TaskKind`, same five values. |
|
||||
| `state` | `state` | `state` | `TaskState`, same eight values. |
|
||||
| `definitionRevision` | `definition_revision` | `definitionRevision` | `int64`, at least 1. |
|
||||
|
||||
@@ -66,6 +66,7 @@ operation 模型;本变更选择接受它可见。此后 D4 已把共用命令
|
||||
| `taskId` | `id` | `taskId` | 与 `PublicSession`、`PublicAction` 一致。 |
|
||||
| (无) | `object` | (无) | 新增,`agent.task`。 |
|
||||
| `sessionId` | `session_id` | `sessionId` | |
|
||||
| (无) | `child_session_id` | `childSessionId` | 仅 child 类型(#13746 F2)。 |
|
||||
| `kind` | `kind` | `kind` | `TaskKind`,同样五个值。 |
|
||||
| `state` | `state` | `state` | `TaskState`,同样八个值。 |
|
||||
| `definitionRevision` | `definition_revision` | `definitionRevision` | `int64`,至少为 1。 |
|
||||
|
||||
@@ -220,7 +220,7 @@ The facts below are from `main` at `ba8615f4c4` (Flyway V60, contract `1.38.0`),
|
||||
| H3 kinds | The Shell half was delivered by A8 (#13889): a platform stop settles `stop_requested` after its request. **Delivered in [H4f-c](2026-10-17-managed-h3-task-cancel.md): both kinds join `CANCELLABLE_TASK_KINDS`, their delivery stops the process through the Runtime Broker, and a Monitor's `stop_requested` settlement stands in for the stop request it does not carry.** |
|
||||
| Workflow, automation | With the workflow runtime (#13803) and H6 per_run: a stop request on their records, then the same delivery and stop arms. |
|
||||
| Channel delivery cancel | With H5's public mutation routes: reuse this slice's operation model (decision 10). |
|
||||
| Child output | Link a child task to its child Session, or relay its output as task events behind `read_output`. |
|
||||
| Child output | **Delivered by the [child Session link](2026-10-17-managed-child-task-session-link.md) (#13746 F2): a child task names its child Session, whose routes serve its output.** Relaying it as task events behind `read_output` was not taken. |
|
||||
| WebShell | The task panel (#13936) and its client methods landed for `child_agent` tasks. **It now lists every task kind and offers cancel wherever the task advertises it ([panel note](2026-10-12-managed-child-task-panel.md)).** |
|
||||
| Stage F | The real-stack round's remaining ordering: a cancel after the owner is gone. |
|
||||
| Close stops the child | The relay's stop arm skips a parent that is no longer `ACTIVE`, and the close cascade waited for each child's Turn to end (H4b). So a close admitted right after a cancel let the child run on. **Delivered in [#13875](2026-10-14-managed-close-stops-child-turn.md): the cascade cancels each child's active Turn itself (option B), and the close no longer waits for the Turn's natural end.** |
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
| H3 类型 | Shell 那一半已由 A8(#13889)交付:平台停止在其请求之后以 `stop_requested` 结算。**已在 [H4f-c](2026-10-17-managed-h3-task-cancel.zh-CN.md) 交付:两类加入 `CANCELLABLE_TASK_KINDS`,其投递经 Runtime Broker 停止进程,Monitor 的 `stop_requested` 结算代替它并不携带的停止请求。** |
|
||||
| workflow、自动化 | 随 workflow 运行时(#13803)与 H6 per_run:先在各自记录上加停止请求,再复用相同的投递与停止分支。 |
|
||||
| channel 投递取消 | 随 H5 的公开变更路由:复用本切片的操作模型(决策 10)。 |
|
||||
| child 输出 | 把 child 任务关联到其 child Session,或在 `read_output` 之后把其输出作为任务事件转发。 |
|
||||
| child 输出 | **已由 [child Session 链接](2026-10-17-managed-child-task-session-link.zh-CN.md)(#13746 F2)交付:child 任务给出其 child Session,由该 Session 的路由提供输出。** 没有采用在 `read_output` 之后作为任务事件转发。 |
|
||||
| WebShell | 任务面板(#13936)及其客户端方法已为 `child_agent` 任务落地。**它现在列出所有任务类型,并在任务公布取消时提供取消([面板文档](2026-10-12-managed-child-task-panel.zh-CN.md))。** |
|
||||
| F 阶段 | 真实栈轮次剩下的时序:所有者消失后的取消。 |
|
||||
| 关闭时停止 child | relay 的停止分支会跳过已不是 `ACTIVE` 的父会话,而关闭级联曾等每个 child 的 Turn 自己结束(H4b),所以取消后紧接着被接纳的关闭会让 child 继续运行。**已由 [#13875](2026-10-14-managed-close-stops-child-turn.zh-CN.md) 交付:级联自己取消每个 child 的活跃 Turn(方案 B),关闭不再等待 Turn 自然结束。** |
|
||||
|
||||
@@ -60,6 +60,6 @@ Checked at `d28bc60d73` (Flyway V68, contract `1.44.0`):
|
||||
| H3 kinds | **Delivered in [H4f-c](2026-10-17-managed-h3-task-cancel.md):** A8 (#13889) already settles a platform-stopped Shell `stop_requested`; both kinds join `CANCELLABLE_TASK_KINDS`, their delivery stops the process through the Runtime Broker, and a Monitor's `stop_requested` settlement is its recorded stop. |
|
||||
| `automation_run` | Carries no stop request; H6's per_run target owns it. |
|
||||
| Channel delivery cancel | With H5's public mutation routes (resend first), reusing this operation model — decision 10 of [the H4f note](2026-10-10-managed-task-cancel.md). |
|
||||
| Child output events | Link a child task to its child Session, or relay its output behind `read_output` — the separate decision the H4f note deferred, unchanged. |
|
||||
| Child output events | Delivered by the [child Session link](2026-10-17-managed-child-task-session-link.md) (#13746 F2): a workflow task names its child Session like a child agent task. |
|
||||
| WebShell panel | **Delivered:** the panel lists workflow tasks with every other kind and admits a cancel wherever the task advertises it ([panel note](2026-10-12-managed-child-task-panel.md)). |
|
||||
| Close stops the child | [#13875](https://github.com/QwenLM/qwen-code/issues/13875), unchanged: the relay's stop arm still skips a parent that is no longer `ACTIVE`. |
|
||||
@@ -60,6 +60,6 @@
|
||||
| H3 类型 | **已在 [H4f-c](2026-10-17-managed-h3-task-cancel.zh-CN.md) 交付:** A8(#13889)已把被平台停止的 Shell 结算为 `stop_requested`;两类加入 `CANCELLABLE_TASK_KINDS`,其投递经 Runtime Broker 停止进程,Monitor 的 `stop_requested` 结算即其已记录的停止。 |
|
||||
| `automation_run` | 不携带停止请求;归 H6 的 per_run 目标所有。 |
|
||||
| Channel delivery 取消 | 随 H5 的公开变更路由(resend 先行)一起做,复用本操作模型 —— 见 [H4f 设计](2026-10-10-managed-task-cancel.zh-CN.md)决策 10。 |
|
||||
| Child 输出事件 | 把 child 任务链接到其 child Session,或在 `read_output` 背后转发其输出 —— H4f 设计推迟的那个独立决策,不变。 |
|
||||
| Child 输出事件 | 已由 [child Session 链接](2026-10-17-managed-child-task-session-link.zh-CN.md)(#13746 F2)交付:workflow 任务像 child agent 任务一样给出其 child Session。 |
|
||||
| WebShell 面板 | **已交付:** 面板与其他所有类型一起列出 workflow 任务,并在任务公布取消时接受取消([面板文档](2026-10-12-managed-child-task-panel.zh-CN.md))。 |
|
||||
| Close 停止 child | [#13875](https://github.com/QwenLM/qwen-code/issues/13875),不变:relay 的停止臂仍跳过不再 `ACTIVE` 的父 Session。 |
|
||||
@@ -0,0 +1,50 @@
|
||||
# Managed child task to child Session link (H4f F2)
|
||||
|
||||
[English](2026-10-17-managed-child-task-session-link.md) | [简体中文](2026-10-17-managed-child-task-session-link.zh-CN.md)
|
||||
|
||||
Status: implemented in this change. A `child_agent` or `workflow` task names the child Session that runs it: `PublicTask.child_session_id` and `WebShellTask.childSessionId`, served in contract `1.55.0` (main is at `1.54.0`; renumber at merge if another pull request takes `1.55.0` first). No new route, authorization rule, migration or event type.
|
||||
|
||||
This is F2 of [#13746](https://github.com/QwenLM/qwen-code/issues/13746), slice H4f of [#12827](https://github.com/QwenLM/qwen-code/issues/12827), stage H of the Managed Agent proposal [#12380](https://github.com/QwenLM/qwen-code/issues/12380). [The H4f note](2026-10-10-managed-task-cancel.md) (decision 9) left two ways to expose a child task's output: link the task to its child Session, or relay the child's output as task events behind `read_output`. This change takes the link.
|
||||
|
||||
## Problem
|
||||
|
||||
Since H4b a child run is a real task. A caller can list it, watch its `state_changed` events and, since H4f, cancel it, but cannot read what it produced. A child task advertises no `read_output`, so its event stream has no `output` events, and its `artifact_refs` stay empty because the child's final text is not published as an Artifact. The output exists: it is the child Session's own transcript. Nothing on the task says which Session that is.
|
||||
|
||||
## Current state
|
||||
|
||||
Checked at `3bddf6e0bb` (contract `1.51.0`, schema V6).
|
||||
|
||||
- **The child Session id lives in the run's record.** The `child_run` body of both child Session kinds carries `childSessionId` (`ManagedExtensionRecords.java:179`). It starts null (`:1126`), is set once (`:1191`), and must be non-null once the run is attached or settled after an attach (`requireChildSession`, `:957`). The attach revision sets it (`childAttachBody`, `managed-child-operations.ts:460`), which the result relay sends once the child is bound (`ChildResultRelay.java:878`). A terminal verdict can also name a minted child that never started (`managed-child-operations.ts:501`). The Shell kind of `child_run` has no such key.
|
||||
- **The task row does not carry it.** `ma_session_record` holds only the projection columns (task kind, state, Runtime state, delivery, times) and a `record_resource_id` that points at the latest committed body. The task view is built from that row alone (`ManagedTaskService.publicTask`/`webShellTask`), and `PublicTask.session_id` is always the parent.
|
||||
- **The child Session is already readable by the task's readers.** A child Session is a plain PUBLIC Session inserted with the parent's tenant and `workspace_id` (`ManagedAgentStore.insertChildSession`, `:663`). Every Session read route goes through `requireReadableSession` (`ManagedAgentService.java:1600`), which checks only that the Session is not deleted and that the caller can read its Workspace. So whoever can read the parent's task can already read the child's Session, Turns, items and events; Session lists do not filter children either. The only missing piece is the child's id.
|
||||
- **Nothing public names a child Session today.** The OpenAPI has no child Session field anywhere.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Link, do not relay.** The task view gains one optional field naming the child Session. The caller reads the child's output through the routes that already serve it: Session, Turns, items, events and their WebShell mirrors. Relaying would copy the transcript into the bounded task journal under the retention floor and the Artifact visibility barrier, and `read_output` is fixed for a task's life; the link needs none of that.
|
||||
2. **Only the two child Session kinds carry it, and only once the record names the child.** `child_agent` and `workflow` tasks carry the field when the run's latest committed body has a non-null `childSessionId`: from the attach on, or on a terminal verdict that names a minted child. Before that the field is absent, as it is for `background_shell`, `monitor` and `automation_run`. The contract states the kind rule as a conditional on both task schemas. The lineage edge on `ma_session` exists from the mint, but the H4b rule is that the parent learns of the child at attach and never by inference, so the view reads the record and not the lineage.
|
||||
3. **The view reads the committed body; there is no new column.** The body is the only place the fact lives. A projected column would need a migration (V7 is already claimed by an open pull request), a backfill for rows committed before it, and a writer change on every revision, to save one inline resource read per child task. The task view already makes one query per task (its event cursor positions), so the extra read keeps the same shape and applies only to child kinds. Only the two view paths read it: the cancel admission's task lookup stays a pure projection read. Record bodies are collected only once their Session is retired, so a revision that lands between the row read and the body read leaves the previous body readable, and the view reports the record as of that previous revision.
|
||||
4. **No new authorization.** The child inherits the parent's Workspace, so the set of callers that can read the task equals the set that can read the child. The link discloses an id only to callers who could already find the child among the Sessions they can list.
|
||||
5. **No final-text publication.** Publishing the child's final text as a task Artifact (the task panel note's follow-up) stays separate. A caller that wants the final answer reads the child's newest assistant item.
|
||||
|
||||
## Contract changes (1.55.0)
|
||||
|
||||
- `PublicTask` gains `child_session_id` and `WebShellTask` gains `childSessionId`: optional, `format: uuid`, served (no marker). Each schema adds a conditional that forbids the field for any kind other than `child_agent` and `workflow`.
|
||||
- `info.description` gains the v1.55 paragraph.
|
||||
- The generated WebShell types gain `childSessionId`.
|
||||
|
||||
## Validation
|
||||
|
||||
- **Real records** (`ManagedExtensionRecordStoreTest`): a `child_agent` chain committed through the Session store names no child before its attach, on both surfaces; from the attach on, detail and list on both surfaces name it, and the settled run still names it. The workflow chain names its child the same way. A background Shell row and a Monitor task name none.
|
||||
- **Contract** (`ManagedAgentApiContractTest`): a served child task carries `child_session_id` on the public route and `childSessionId` on the WebShell route, each validated against its schema; a workflow task whose record names no child omits it. `recordsMatchTheirSchemas` pins the DTO components against the schema properties.
|
||||
- **Schema** (`PlannedTaskContractTest`): both child kinds accept the field and its absence, a malformed id is refused, and `background_shell`, `monitor` and `automation_run` refuse it, on both surfaces. Removing the kind conditional fails these cases.
|
||||
- **Types**: the regenerated WebShell types pass their freshness test, and the WebShell package typechecks.
|
||||
- **Real stack** (FG9, `HostedSessionRelayFaultGatesIT`, MySQL 8.4, packaged Harness): in all six cases, a team member runs as a `child_agent` task of its lead. The lead's public task view names the member's child Session in `child_session_id`, the same Session the member's committed `child_run` record attached, and that Session reads through the public Session route. Both letters pass.
|
||||
- **Not run:** the MySQL integration lane, and reading the child's Turns and Items through the link beyond the Session itself.
|
||||
|
||||
## Follow-up work
|
||||
|
||||
| Item | Scope |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| WebShell task panel | Show the link in the task detail and open the child Session's transcript from it. |
|
||||
| Final-text publication | Publish a child's final text as a task Artifact, so `artifact_refs` carries it ([task panel note](2026-10-12-managed-child-task-panel.md), follow-up). |
|
||||
@@ -0,0 +1,50 @@
|
||||
# Managed child 任务到 child Session 的链接(H4f F2)
|
||||
|
||||
[English](2026-10-17-managed-child-task-session-link.md) | [简体中文](2026-10-17-managed-child-task-session-link.zh-CN.md)
|
||||
|
||||
状态:已在本变更中实现。`child_agent` 或 `workflow` 任务会给出运行它的 child Session:`PublicTask.child_session_id` 与 `WebShellTask.childSessionId`,在契约 `1.55.0` 中提供(main 当前为 `1.54.0`;若其他 PR 先占用 `1.55.0`,合并时改号)。没有新路由、新授权规则、新迁移或新事件类型。
|
||||
|
||||
这是 [#13746](https://github.com/QwenLM/qwen-code/issues/13746) 的 F2,属于 [#12827](https://github.com/QwenLM/qwen-code/issues/12827) 的 H4f 切片,即 Managed Agent 提案 [#12380](https://github.com/QwenLM/qwen-code/issues/12380) 的 H 阶段。[H4f 设计](2026-10-10-managed-task-cancel.zh-CN.md)(决策 9)为暴露 child 任务的输出留了两条路:把任务链接到其 child Session,或在 `read_output` 之后把 child 的输出作为任务事件转发。本变更选择链接。
|
||||
|
||||
## 问题
|
||||
|
||||
自 H4b 起,一次 child run 就是一个真实的任务。调用者可以列出它、观察它的 `state_changed` 事件,自 H4f 起还能取消它,却读不到它产出了什么。child 任务不公布 `read_output`,所以它的事件流里没有 `output` 事件;child 的最终文本也没有作为 Artifact 发布,所以它的 `artifact_refs` 一直为空。输出本身是存在的:就是 child Session 自己的对话记录。只是任务上没有任何字段说明是哪个 Session。
|
||||
|
||||
## 现状
|
||||
|
||||
在 `3bddf6e0bb`(契约 `1.51.0`,schema V6)上核实。
|
||||
|
||||
- **child Session ID 记在 run 的记录里。** 两种 child Session 类型的 `child_run` 记录体都带 `childSessionId`(`ManagedExtensionRecords.java:179`)。它起始为 null(`:1126`),只设一次(`:1191`),一旦 run 已 attach,或在 attach 之后结算,它就必须非空(`requireChildSession`,`:957`)。设置它的是 attach 修订(`childAttachBody`,`managed-child-operations.ts:460`),由结果中继在 child 绑定后发出(`ChildResultRelay.java:878`)。终态裁决也可能点名一个已登记但从未启动的 child(`managed-child-operations.ts:501`)。`child_run` 的 Shell 类型没有这个键。
|
||||
- **任务行不带它。** `ma_session_record` 只存投影列(任务类型、状态、Runtime 状态、投递、时间)以及指向最新已提交记录体的 `record_resource_id`。任务视图只用这一行构建(`ManagedTaskService.publicTask`/`webShellTask`),且 `PublicTask.session_id` 总是父会话。
|
||||
- **child Session 本来就对任务的读者可读。** child Session 是一个普通的 PUBLIC Session,插入时沿用父会话的租户与 `workspace_id`(`ManagedAgentStore.insertChildSession`,`:663`)。所有 Session 读取路由都经过 `requireReadableSession`(`ManagedAgentService.java:1600`),它只检查 Session 未删除、调用者能读其 Workspace。所以能读父会话任务的人,本来就能读 child 的 Session、Turn、item 与事件;Session 列表也不过滤 child。唯一缺的是 child 的 ID。
|
||||
- **目前没有任何公开字段给出 child Session。** OpenAPI 里任何地方都没有 child Session 字段。
|
||||
|
||||
## 决策
|
||||
|
||||
1. **链接,而不是转发。** 任务视图增加一个可选字段,给出 child Session。调用者通过本来就提供这些内容的路由读取 child 的输出:Session、Turn、item、事件以及它们的 WebShell 镜像。转发会把对话记录复制进有界的任务日志,并受保留下限与 Artifact 可见性屏障约束,而 `read_output` 在任务生命周期内固定不变;链接不需要这些。
|
||||
2. **只有两种 child Session 类型带它,并且只在记录给出 child 之后。** 当 run 最新已提交的记录体中 `childSessionId` 非空时,`child_agent` 与 `workflow` 任务带这个字段:从 attach 起,或在点名了已登记 child 的终态裁决上。在此之前字段缺省,`background_shell`、`monitor` 与 `automation_run` 也一样缺省。契约在两个任务 schema 上都把类型规则写成条件约束。`ma_session` 上的谱系边从登记时就存在,但 H4b 的规则是父会话在 attach 时得知 child、绝不靠推断,所以视图读记录而不读谱系。
|
||||
3. **视图读已提交的记录体;不新增列。** 这个事实只存在于记录体里。投影列需要一次迁移(V7 已被一个在飞 PR 占用)、对迁移前已提交的行做回填,以及在每次修订时改写写入路径,换来的只是每个 child 任务少读一次内联资源。任务视图本来就对每个任务做一次查询(其事件游标位置),多出的这次读取保持同样的形态,且只针对 child 类型。只有两条视图路径会读它:取消准入的任务查找仍是纯投影读取。记录体只有在其 Session 退役后才会被回收,所以在读行与读记录体之间落下的修订会让上一版记录体仍可读,视图按那一版报告记录。
|
||||
4. **不新增授权。** child 沿用父会话的 Workspace,所以能读任务的调用者集合与能读 child 的集合相同。这个链接只把 ID 暴露给本来就能在其可列出的 Session 中找到该 child 的调用者。
|
||||
5. **不发布最终文本。** 把 child 的最终文本发布为任务 Artifact(任务面板设计的后续项)仍是单独的工作。想要最终答复的调用者读取 child 最新的助手 item。
|
||||
|
||||
## 契约变更(1.55.0)
|
||||
|
||||
- `PublicTask` 增加 `child_session_id`,`WebShellTask` 增加 `childSessionId`:可选、`format: uuid`、已提供(无标记)。两个 schema 各增加一个条件约束:除 `child_agent` 与 `workflow` 外的任何类型都禁止出现该字段。
|
||||
- `info.description` 增加 v1.55 段落。
|
||||
- 重新生成的 WebShell 类型增加 `childSessionId`。
|
||||
|
||||
## 验证
|
||||
|
||||
- **真实记录**(`ManagedExtensionRecordStoreTest`):经 Session store 提交的 `child_agent` 链在 attach 之前,两个接口面都不给出 child;从 attach 起,两个接口面的详情与列表都给出它,结算后的 run 仍给出它。workflow 链以同样方式给出其 child。后台 Shell 行与 Monitor 任务都不给出。
|
||||
- **契约**(`ManagedAgentApiContractTest`):实际提供的 child 任务在公共路由上带 `child_session_id`,在 WebShell 路由上带 `childSessionId`,各自按其 schema 校验;记录尚未给出 child 的 workflow 任务不带该字段。`recordsMatchTheirSchemas` 把 DTO 分量与 schema 属性对齐钉住。
|
||||
- **Schema**(`PlannedTaskContractTest`):两种 child 类型都接受该字段及其缺省,格式错误的 ID 被拒绝,`background_shell`、`monitor` 与 `automation_run` 带该字段被拒绝,两个接口面都如此。去掉类型条件约束会让这些用例失败。
|
||||
- **类型**:重新生成的 WebShell 类型通过其新鲜度测试,WebShell 包通过类型检查。
|
||||
- **真实栈**(FG9,`HostedSessionRelayFaultGatesIT`,MySQL 8.4,打包的 Harness):六个用例中,team 成员都作为其 lead 的 `child_agent` 任务运行。lead 的公开任务视图在 `child_session_id` 中给出成员的 child Session,它正是成员已提交的 `child_run` 记录所 attach 的那个 Session,并且该 Session 可经公开 Session 路由读取。两个字母均通过。
|
||||
- **未运行:** MySQL 集成测试车道,以及在 Session 本身之外经由链接读取 child 的 Turn 与 Item。
|
||||
|
||||
## 后续工作
|
||||
|
||||
| 项 | 范围 |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| WebShell 任务面板 | 在任务详情中展示链接,并从它打开 child Session 的对话记录。 |
|
||||
| 最终文本发布 | 把 child 的最终文本发布为任务 Artifact,使 `artifact_refs` 带上它([任务面板设计](2026-10-12-managed-child-task-panel.zh-CN.md)的后续项)。 |
|
||||
+4
-2
@@ -499,7 +499,8 @@ public final class ApiModels {
|
||||
@JsonProperty("output_cursor") String outputCursor,
|
||||
@JsonProperty("artifact_refs") List<String> artifactRefs,
|
||||
@JsonProperty("action_capabilities")
|
||||
List<String> actionCapabilities) {
|
||||
List<String> actionCapabilities,
|
||||
@JsonProperty("child_session_id") String childSessionId) {
|
||||
}
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@@ -507,7 +508,8 @@ public final class ApiModels {
|
||||
String state, Long definitionRevision, String runtimeState,
|
||||
long createdAt, Long startedAt, Long settledAt,
|
||||
String outputCursor,
|
||||
List<String> artifactRefs, List<String> actionCapabilities) {
|
||||
List<String> artifactRefs, List<String> actionCapabilities,
|
||||
String childSessionId) {
|
||||
}
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
|
||||
+4
-2
@@ -228,7 +228,8 @@ public class ManagedTaskService {
|
||||
view.runtimeState(), view.createdAt(), view.startedAt(),
|
||||
view.settledAt(), outputCursor(tenantId, sessionId, task.taskId(), positions),
|
||||
positions.artifactRefs(),
|
||||
actions(task, view, sessionActive));
|
||||
actions(task, view, sessionActive),
|
||||
records.childSessionId(tenantId, sessionId, task));
|
||||
}
|
||||
|
||||
private WebShellTask webShellTask(String tenantId, String sessionId,
|
||||
@@ -241,7 +242,8 @@ public class ManagedTaskService {
|
||||
view.createdAt(), view.startedAt(), view.settledAt(),
|
||||
outputCursor(tenantId, sessionId, task.taskId(), positions),
|
||||
positions.artifactRefs(),
|
||||
actions(task, view, sessionActive));
|
||||
actions(task, view, sessionActive),
|
||||
records.childSessionId(tenantId, sessionId, task));
|
||||
}
|
||||
|
||||
/** The task's own actions, and none in a Session that is not active:
|
||||
|
||||
+22
-2
@@ -91,6 +91,9 @@ public class ManagedExtensionRecordStore {
|
||||
+ "):[0-9]+$");
|
||||
private static final Pattern TASK_ID = Pattern.compile(
|
||||
"^task_([0-9a-f]{64})$");
|
||||
/** The task kinds a child Session of their own runs (H4b, H4c). */
|
||||
private static final Set<String> CHILD_SESSION_TASK_KINDS = Set.of(
|
||||
"child_agent", "workflow");
|
||||
private static final Set<String> SESSION_KEY_FIELDS = Set.of("tenantId",
|
||||
"workspaceId", "sessionId");
|
||||
private static final Set<String> PAYLOAD_FIELDS = Set.of("domain",
|
||||
@@ -132,8 +135,9 @@ public class ManagedExtensionRecordStore {
|
||||
this(jdbc, sessions, new ManagedTaskEventStore(jdbc));
|
||||
}
|
||||
|
||||
/** A task's projection row and the resource holding its record's latest committed body. */
|
||||
public record TaskRow(String taskId, String kind,
|
||||
TaskProjection projection) {
|
||||
TaskProjection projection, String resourceId) {
|
||||
}
|
||||
|
||||
public record TaskPage(List<TaskRow> tasks, boolean hasMore) {
|
||||
@@ -780,6 +784,21 @@ public class ManagedExtensionRecordStore {
|
||||
recordKey).stream().findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* #13746 F2: the child Session that runs a child agent or workflow task, once the run's
|
||||
* committed record names it (the attach, or a verdict that names a minted child), else null.
|
||||
* The record body is the only place that fact lives; no other kind has a child Session.
|
||||
*/
|
||||
public String childSessionId(String tenantId, String sessionId,
|
||||
TaskRow task) {
|
||||
if (!CHILD_SESSION_TASK_KINDS.contains(task.kind())) {
|
||||
return null;
|
||||
}
|
||||
JsonNode named = readBody(readResource(tenantId, sessionId,
|
||||
task.resourceId())).get("childSessionId");
|
||||
return named == null || named.isNull() ? null : named.textValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* The event-level checks the Session authority's reader runs on every event line: a closed
|
||||
* envelope with an optional subject, version 1, the sequence of the line's place among the
|
||||
@@ -1434,7 +1453,8 @@ public class ManagedExtensionRecordStore {
|
||||
}
|
||||
return new TaskRow(ManagedExtensionProjection.taskId(
|
||||
result.getString("record_key")),
|
||||
result.getString("task_kind"), projection(result));
|
||||
result.getString("task_kind"), projection(result),
|
||||
result.getString("record_resource_id"));
|
||||
}
|
||||
|
||||
private static StoredRow storedRow(ResultSet result, int row)
|
||||
|
||||
+42
-2
@@ -2,8 +2,8 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Qwen Managed Agent Public and WebShell API",
|
||||
"version": "1.54.0",
|
||||
"description": "Canonical contract for the public Managed Agent resources and the WebShell adapter. partial means that the route exists but generated types and full contract tests are pending; planned means that the route or field is not implemented. Workspace context and cwd operations are v1.9 target contracts, not current server capabilities. v1.10 adds planned command admission, Actions, lifecycle and actor authorization. v1.12 refines planned W0 creation discovery, actor-specific create hints and explicit default selection. Filter planned fields as well as routes from production SDKs. v1.13 moves this contract into the Qwen Code repository as its single source and records the shipped rename, archive, unarchive and delete routes as partial. v1.14 integrates W0d workspace discovery, the WebShell workspace lookup, and empty-session binding as partial; workspace_binding/workspaceBinding advertises this narrow flow without enabling workspace execution or context switching. v1.15 adds the archived, archiving and deleting Session statuses and the cancelling Turn status, requires request_id in error envelopes, declares the error responses that the server returns, and marks Session create, list and get on both surfaces implemented. v1.16 adds the planned Stage H task contract (SessionTaskView as PublicTask, task list, detail, events and cancel) and names the MCP catalog, hook catalog, automation and channel resources as planned; their shapes arrive with Stages H1 to H6. v1.17 implements event replay: events carry their schema and projection versions and a top-level Item and Part identity, JSON event pages return has_more and next_cursor and accept limits up to 1000, and a persisted replay floor answers expired cursors with 409 cursor_expired in JSON and one agent.session.resync_required frame over SSE; it marks the public event query and stream, the WebShell event stream and the WebShell transcript implemented. v1.18 implements the durable Session lifecycle (Stage D4): close, archive and delete answer 202 with a command operation on both surfaces, an archive requires a closed Session, a deleted Session leaves a tombstone whose operations stay readable, and the operation query serves them; unarchive restores a closed Session, and Session capabilities advertise session_lifecycle. v1.19 serves the task list and detail on both surfaces as partial (Stage H0c): the Session store projects them from the Stage H records the Session authority commits, a task.updated Session event announces each change of a task's view, and Session capabilities advertise tasks. Task events and cancel stay planned until a slice's tasks produce output and accept a cancel. v1.20 implements the Turn read model (Stage D5): the public Turn list and detail read a Session's Turns without their input, newest first by creation time in milliseconds and then Turn ID, with an opaque cursor; a deleted Session's Turns are not readable, and the durable admission fields of a Turn stay planned until D7. v1.21 declares on the task list, detail and event routes of both surfaces the 403 actor_scope_mismatch that the tenant filter answers on every /v1/agents/ and WebShell route, as the Session and Turn reads already do, and the shared Forbidden response names it; a caller that cannot read a task still gets 404. v1.22 adds deployment-opted-in creation with an initial Workspace file-tool Turn (G0) on the public and WebShell surfaces; the fixed server-owned profile does not enable later Workspace operations. v1.23 settles the planned task event and cancel semantics: a durable retention floor, committed-prefix publication, stable cursors, Artifact visibility before expiry, bounded backlog under archival failure, ordered idempotent cancellation and command outcomes. Clients tolerate unknown optional event fields across minor versions. Task events and cancel remain planned. v1.24 serves the read-only Session MCP catalog as partial (Stage H1), projecting display metadata and schemas from committed records without Runtime identities, credentials or production profile enablement. v1.25 implements permission Actions (Stage D6b) on both surfaces: list, get and respond, projected from the Session's action.changed records; a WebShell permission Action carries inputRevision, policyRevision, functionCallId, toolName and expiresAt, its option ids allow and deny are stable, a Turn has at most one requested approval at a time, and a response is a durable command operation carrying requestId. v1.26 declares the tenant filter's 403 actor_scope_mismatch on every covered public and WebShell route, including planned routes, and pins the refusal code in contract traffic. v1.27 adds O3 durable tool-result projection, artifact metadata, immutable range/streaming content and WebShell result reads. Artifact capability requires a configured O3 reader and a Workspace-bound Session; execution admission remains independent. v1.28 admits later Turns of a Workspace-bound Session for the SLine truncated
|
||||
"version": "1.55.0",
|
||||
"description": "Canonical contract for the public Managed Agent resources and the WebShell adapter. partial means that the route exists but generated types and full contract tests are pending; planned means that the route or field is not implemented. Workspace context and cwd operations are v1.9 target contracts, not current server capabilities. v1.10 adds planned command admission, Actions, lifecycle and actor authorization. v1.12 refines planned W0 creation discovery, actor-specific create hints and explicit default selection. Filter planned fields as well as routes from production SDKs. v1.13 moves this contract into the Qwen Code repository as its single source and records the shipped rename, archive, unarchive and delete routes as partial. v1.14 integrates W0d workspace discovery, the WebShell workspace lookup, and empty-session binding as partial; workspace_binding/workspaceBinding advertises this narrow flow without enabling workspace execution or context switching. v1.15 adds the archived, archiving and deleting Session statuses and the cancelling Turn status, requires request_id in error envelopes, declares the error responses that the server returns, and marks Session create, list and get on both surfaces implemented. v1.16 adds the planned Stage H task contract (SessionTaskView as PublicTask, task list, detail, events and cancel) and names the MCP catalog, hook catalog, automation and channel resources as planned; their shapes arrive with Stages H1 to H6. v1.17 implements event replay: events carry their schema and projection versions and a top-level Item and Part identity, JSON event pages return has_more and next_cursor and accept limits up to 1000, and a persisted replay floor answers expired cursors with 409 cursor_expired in JSON and one agent.session.resync_required frame over SSE; it marks the public event query and stream, the WebShell event stream and the WebShell transcript implemented. v1.18 implements the durable Session lifecycle (Stage D4): close, archive and delete answer 202 with a command operation on both surfaces, an archive requires a closed Session, a deleted Session leaves a tombstone whose operations stay readable, and the operation query serves them; unarchive restores a closed Session, and Session capabilities advertise session_lifecycle. v1.19 serves the task list and detail on both surfaces as partial (Stage H0c): the Session store projects them from the Stage H records the Session authority commits, a task.updated Session event announces each change of a task's view, and Session capabilities advertise tasks. Task events and cancel stay planned until a slice's tasks produce output and accept a cancel. v1.20 implements the Turn read model (Stage D5): the public Turn list and detail read a Session's Turns without their input, newest first by creation time in milliseconds and then Turn ID, with an opaque cursor; a deleted Session's Turns are not readable, and the durable admission fields of a Turn stay planned until D7. v1.21 declares on the task list, detail and event routes of both surfaces the 403 actor_scope_mismatch that the tenant filter answers on every /v1/agents/ and WebShell route, as the Session and Turn reads already do, and the shared Forbidden response names it; a caller that cannot read a task still gets 404. v1.22 adds deployment-opted-in creation with an initial Workspace file-tool Turn (G0) on the public and WebShell surfaces; the fixed server-owned profile does not enable later Workspace operations. v1.23 settles the planned task event and cancel semantics: a durable retention floor, committed-prefix publication, stable cursors, Artifact visibility before expiry, bounded backlog under archival failure, ordered idempotent cancellation and command outcomes. Clients tolerate unknown optional event fields across minor versions. Task events and cancel remain planned. v1.24 serves the read-only Session MCP catalog as partial (Stage H1), projecting display metadata and schemas from committed records without Runtime identities, credentials or production profile enablement. v1.25 implements permission Actions (Stage D6b) on both surfaces: list, get and respond, projected from the Session's action.changed records; a WebShell permission Action carries inputRevision, policyRevision, functionCallId, toolName and expiresAt, its option ids allow and deny are stable, a Turn has at most one requested approval at a time, and a response is a durable command operation carrying requestId. v1.26 declares the tenant filter's 403 actor_scope_mismatch on every covered public and WebShell route, including planned routes, and pins the refusal code in contract traffic. v1.27 adds O3 durable tool-result projection, artifact metadata, immutable range/streaming content and WebShell result reads. Artifact capability requires a configured O3 reader and a Workspace-bound Session; execution admission remains independent. v1.28 admits later Turns of a Workspace-bound Session for the SLine truncated
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@@ -8434,6 +8434,11 @@
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"child_session_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The child Session that runs this task: present for a child_agent or workflow task once the run's committed record names it (the child's attach, or a terminal verdict that names a minted child), and absent for every other kind and before then. The child is a Session in the same Workspace, readable by exactly the callers that can read this task, so its Turns, items and events are the task's own output; reading them needs no other grant."
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/components/schemas/TaskKind"
|
||||
},
|
||||
@@ -8567,6 +8572,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": ["child_agent", "workflow"]
|
||||
}
|
||||
},
|
||||
"required": ["kind"]
|
||||
},
|
||||
"else": {
|
||||
"not": {
|
||||
"required": ["child_session_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Read-only projection of one asynchronous task of a Session (SessionTaskView). It is rebuilt from the Session's durable domain records and is never their source of truth. It never carries a Runtime binding ID, generation, Runtime endpoint, Pod, absolute path, raw PID, SecretHandle or local sidecar; diagnostics are authorized Artifact references."
|
||||
@@ -8789,6 +8809,11 @@
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"childSessionId": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The child Session that runs this task: present for a child_agent or workflow task once the run's committed record names it (the child's attach, or a terminal verdict that names a minted child), and absent for every other kind and before then. The child is a Session in the same Workspace, readable by exactly the callers that can read this task, so its Turns, items and events are the task's own output; reading them needs no other grant."
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/components/schemas/TaskKind"
|
||||
},
|
||||
@@ -8922,6 +8947,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": ["child_agent", "workflow"]
|
||||
}
|
||||
},
|
||||
"required": ["kind"]
|
||||
},
|
||||
"else": {
|
||||
"not": {
|
||||
"required": ["childSessionId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Read-only projection of one asynchronous task of a Session (SessionTaskView). It is rebuilt from the Session's durable domain records and is never their source of truth. It never carries a Runtime binding ID, generation, Runtime endpoint, Pod, absolute path, raw PID, SecretHandle or local sidecar; diagnostics are authorized Artifact references."
|
||||
|
||||
+21
-1
@@ -9,6 +9,7 @@ import com.alibaba.qwen.code.managedagent.api.AuthenticatedTenantActor;
|
||||
import com.alibaba.qwen.code.managedagent.api.WorkspaceSelection;
|
||||
import com.alibaba.qwen.code.managedagent.service.EmbeddedRuntimeBroker;
|
||||
import com.alibaba.qwen.code.managedagent.store.ManagedAgentStore;
|
||||
import com.alibaba.qwen.code.managedagent.store.ManagedExtensionProjection;
|
||||
import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -684,8 +685,10 @@ class HostedSessionRelayFaultGatesIT {
|
||||
|
||||
/** One outbound chain on the member, exactly one inbound receipt at
|
||||
* revision 1 on the lead, one delivery ledger row. */
|
||||
private void assertSingleCommandChain(String lead, String member) {
|
||||
private void assertSingleCommandChain(String lead, String member)
|
||||
throws Exception {
|
||||
String session = memberSession(lead, member);
|
||||
assertTaskLinksChildSession(lead, member, session);
|
||||
List<Map<String, Object>> rows = jdbc.queryForList(
|
||||
"SELECT record_id, revision FROM ma_session_record"
|
||||
+ " WHERE tenant_id = ? AND session_id = ? AND"
|
||||
@@ -1643,6 +1646,23 @@ class HostedSessionRelayFaultGatesIT {
|
||||
});
|
||||
}
|
||||
|
||||
/** #13746 F2: the member's task on the lead names the member's child
|
||||
* Session publicly, and that Session reads through the public route. */
|
||||
private void assertTaskLinksChildSession(String lead, String member,
|
||||
String session) throws Exception {
|
||||
String runId = childRunIdOf(lead, member);
|
||||
String taskId = "task_" + ManagedExtensionProjection.recordKey(lead,
|
||||
"child_run", runId);
|
||||
JsonNode task = request("GET", "/v1/agents/sessions/" + lead
|
||||
+ "/tasks/" + taskId, null, null, 200);
|
||||
assertThat(task.path("kind").asText()).isEqualTo("child_agent");
|
||||
assertThat(task.path("child_session_id").asText())
|
||||
.as("the task view links the member's child Session")
|
||||
.isEqualTo(session);
|
||||
assertThat(request("GET", "/v1/agents/sessions/" + session, null,
|
||||
null, 200).path("id").asText()).isEqualTo(session);
|
||||
}
|
||||
|
||||
private JsonNode request(String method, String path, Object body,
|
||||
String key, int expected) throws Exception {
|
||||
return request("http://127.0.0.1:" + port, method, path, body, key,
|
||||
|
||||
+40
@@ -1796,6 +1796,28 @@ class ManagedAgentApiContractTest {
|
||||
rest.at("/data/0/id").asText()));
|
||||
}
|
||||
|
||||
/** A committed record body for a synthetic child_run row. */
|
||||
private void childRunResource(String tenant, String sessionId,
|
||||
String resourceId, String json) {
|
||||
String scope = com.alibaba.qwen.code.managedagent.store
|
||||
.ManagedSessionStore.sessionScopeKey(tenant, sessionId);
|
||||
byte[] body = json.getBytes(StandardCharsets.UTF_8);
|
||||
jdbc.update("INSERT INTO ma_resource (resource_key,"
|
||||
+ " session_scope_key, tenant_id, workspace_id, session_id,"
|
||||
+ " resource_id, kind, schema_version, byte_length, sha256,"
|
||||
+ " storage_kind, inline_bytes, publish_command_id, state,"
|
||||
+ " gmt_create) VALUES (?, ?, ?, 'workspace-contract', ?, ?,"
|
||||
+ " 'managed-child-run', 1, ?, ?, 'MYSQL_INLINE', ?,"
|
||||
+ " 'command', 'REFERENCED', "
|
||||
+ com.alibaba.qwen.code.runtimebroker.managedschema
|
||||
.ManagedSchemaTime.SQL_NOW + ")",
|
||||
com.alibaba.qwen.code.runtimebroker.managedschema
|
||||
.ManagedSchemaKeys.key(scope, resourceId),
|
||||
scope, tenant, sessionId, resourceId, body.length,
|
||||
org.apache.commons.codec.digest.DigestUtils.sha256Hex(body),
|
||||
body);
|
||||
}
|
||||
|
||||
/**
|
||||
* H4f: the cancel routes over a monitor task (no cancel action) and a
|
||||
* running child-agent task row. Delivery is the coordinator's and is
|
||||
@@ -1819,12 +1841,26 @@ class ManagedAgentApiContractTest {
|
||||
com.alibaba.qwen.code.managedagent.store.ManagedSessionStore
|
||||
.sessionScopeKey(tenant, sessionId),
|
||||
recordKey, tenant, sessionId, "d".repeat(64));
|
||||
// #13746 F2: the run's committed record names its child Session,
|
||||
// and both task views carry that link inside their schemas.
|
||||
String childSession = UUID.randomUUID().toString();
|
||||
childRunResource(tenant, sessionId, "resource-run-contract",
|
||||
"{\"kind\":\"child_agent\",\"childSessionId\":\""
|
||||
+ childSession + "\"}");
|
||||
String childTask = "task_" + recordKey;
|
||||
JsonNode view = json(exchange(drift, "getSessionTask", 200,
|
||||
get("/v1/agents/sessions/{id}/tasks/{task}", sessionId,
|
||||
childTask).header(TENANT, tenant), null));
|
||||
assertThat(view.get("action_capabilities").toString())
|
||||
.isEqualTo("[\"cancel\"]");
|
||||
assertThat(view.get("child_session_id").asText())
|
||||
.isEqualTo(childSession);
|
||||
JsonNode webShellView = json(exchange(drift, "getWebShellTask", 200,
|
||||
post(WEB_SHELL + "/tasks/get").header(TENANT, tenant),
|
||||
"{\"sessionId\":\"%s\",\"taskId\":\"%s\"}"
|
||||
.formatted(sessionId, childTask)));
|
||||
assertThat(webShellView.get("childSessionId").asText())
|
||||
.isEqualTo(childSession);
|
||||
String cancelPath = "/v1/agents/sessions/{id}/tasks/{task}/cancel";
|
||||
JsonNode admitted = json(exchange(drift, "cancelSessionTask", 202,
|
||||
post(cancelPath, sessionId, childTask).header(TENANT, tenant)
|
||||
@@ -1929,12 +1965,16 @@ class ManagedAgentApiContractTest {
|
||||
com.alibaba.qwen.code.managedagent.store.ManagedSessionStore
|
||||
.sessionScopeKey(tenant, sessionId),
|
||||
workflowKey, tenant, sessionId, "f".repeat(64));
|
||||
childRunResource(tenant, sessionId, "resource-run-contract-wf",
|
||||
"{\"kind\":\"workflow\",\"childSessionId\":null}");
|
||||
String workflowTask = "task_" + workflowKey;
|
||||
JsonNode workflowView = json(exchange(drift, "getSessionTask", 200,
|
||||
get("/v1/agents/sessions/{id}/tasks/{task}", sessionId,
|
||||
workflowTask).header(TENANT, tenant), null));
|
||||
assertThat(workflowView.get("action_capabilities").toString())
|
||||
.isEqualTo("[\"cancel\"]");
|
||||
// Before its child is known, a child task names none.
|
||||
assertThat(workflowView.has("child_session_id")).isFalse();
|
||||
// The child_agent cancel above stays open in this fixture, and the
|
||||
// one open operation per Session rule spans task kinds.
|
||||
assertThat(json(exchange(drift, "cancelSessionTask", 409,
|
||||
|
||||
+27
-2
@@ -137,6 +137,8 @@ class ManagedExtensionRecordStoreTest {
|
||||
ManagedExtensionRecordStore.TaskRow row = records
|
||||
.findTask(TENANT, sessionId, taskId).orElseThrow();
|
||||
assertThat(row.kind()).isEqualTo("background_shell");
|
||||
// #13746 F2: only a child Session kind names a child Session.
|
||||
assertThat(records.childSessionId(TENANT, sessionId, row)).isNull();
|
||||
assertThat(row.projection().state()).isEqualTo("waiting");
|
||||
assertThat(row.projection().runtimeState()).isEqualTo("ready");
|
||||
assertThat(row.projection().startedAt()).isEqualTo(2_000L);
|
||||
@@ -712,7 +714,23 @@ class ManagedExtensionRecordStoreTest {
|
||||
dispatching.withObject("/run").put("execution", "dispatch_started");
|
||||
dispatching.putNull("childSessionId");
|
||||
commitDomain(journal, "stop-2", "child_run", dispatching, List.of());
|
||||
// #13746 F2: no child Session is named before the attach records it.
|
||||
assertThat(tasks.getPublicTask(TENANT, TENANT, sessionId, taskId)
|
||||
.childSessionId()).isNull();
|
||||
assertThat(tasks.getWebShellTask(TENANT, TENANT, sessionId, taskId)
|
||||
.childSessionId()).isNull();
|
||||
commitDomain(journal, "stop-3", "child_run", attached, List.of());
|
||||
// From the attach on, both surfaces name it, on detail and list.
|
||||
assertThat(tasks.getPublicTask(TENANT, TENANT, sessionId, taskId)
|
||||
.childSessionId()).isEqualTo("session-child");
|
||||
assertThat(tasks.listPublicTasks(TENANT, TENANT, sessionId, null, 10)
|
||||
.data().getFirst().childSessionId())
|
||||
.isEqualTo("session-child");
|
||||
assertThat(tasks.getWebShellTask(TENANT, TENANT, sessionId, taskId)
|
||||
.childSessionId()).isEqualTo("session-child");
|
||||
assertThat(tasks.queryWebShellTasks(TENANT, TENANT, sessionId, null,
|
||||
10).data().getFirst().childSessionId())
|
||||
.isEqualTo("session-child");
|
||||
ObjectNode stopping = attached.deepCopy();
|
||||
stopping.put("stopRequested", true);
|
||||
commitDomain(journal, "stop-4", "child_run", stopping, List.of());
|
||||
@@ -754,6 +772,8 @@ class ManagedExtensionRecordStoreTest {
|
||||
var settled = tasks.getPublicTask(TENANT, TENANT, sessionId, taskId);
|
||||
assertThat(settled.state()).isEqualTo("cancelled");
|
||||
assertThat(settled.actionCapabilities()).isEmpty();
|
||||
// The settled run keeps naming the child that carries its output.
|
||||
assertThat(settled.childSessionId()).isEqualTo("session-child");
|
||||
var events = tasks.listPublicTaskEvents(TENANT, TENANT, sessionId,
|
||||
taskId, null, 20);
|
||||
assertThat(events.data())
|
||||
@@ -798,6 +818,9 @@ class ManagedExtensionRecordStoreTest {
|
||||
commitDomain(journal, "stop-2", "child_run", dispatching,
|
||||
List.of());
|
||||
commitDomain(journal, "stop-3", "child_run", attached, List.of());
|
||||
// #13746 F2: a workflow child names its Session like a child agent.
|
||||
assertThat(tasks.getPublicTask(TENANT, TENANT, sessionId, taskId)
|
||||
.childSessionId()).isEqualTo("session-child");
|
||||
ObjectNode stopping = attached.deepCopy();
|
||||
stopping.put("stopRequested", true);
|
||||
commitDomain(journal, "stop-4", "child_run", stopping, List.of());
|
||||
@@ -1667,8 +1690,10 @@ class ManagedExtensionRecordStoreTest {
|
||||
assertThat(seen).as("limit %d", limit).isEqualTo(first);
|
||||
}
|
||||
}
|
||||
assertThat(tasks.getPublicTask(TENANT, null, sessionId,
|
||||
first.get(0).substring(5)).kind()).isEqualTo("monitor");
|
||||
PublicTask monitor = tasks.getPublicTask(TENANT, null, sessionId,
|
||||
first.get(0).substring(5));
|
||||
assertThat(monitor.kind()).isEqualTo("monitor");
|
||||
assertThat(monitor.childSessionId()).isNull();
|
||||
assertThatThrownBy(() -> tasks.listPublicTasks(TENANT, null,
|
||||
sessionId, "not-a-cursor", 1))
|
||||
.hasFieldOrPropertyWithValue("code", "invalid_cursor");
|
||||
|
||||
+23
@@ -97,6 +97,29 @@ class PlannedTaskContractTest {
|
||||
assertThat(failures).isEmpty();
|
||||
}
|
||||
|
||||
// #13746 F2: only a child Session kind names its child Session, and the
|
||||
// name is a Session id.
|
||||
@Test
|
||||
void onlyAChildTaskNamesItsChildSession() {
|
||||
for (String kind : List.of("child_agent", "workflow")) {
|
||||
ObjectNode linked = task("running", 2L, null, "cancel")
|
||||
.put("kind", kind).put("child_session_id",
|
||||
"0d7f3c2e-5b6a-4e1f-8a9b-abcdef012345");
|
||||
accept(kind + " names its child Session", linked);
|
||||
accept(kind + " before its child is known", task("running", 2L,
|
||||
null, "cancel").put("kind", kind));
|
||||
reject(kind + " with a malformed child Session", linked.deepCopy()
|
||||
.put("child_session_id", "not-a-session"));
|
||||
}
|
||||
for (String kind : List.of("background_shell", "monitor",
|
||||
"automation_run")) {
|
||||
reject(kind + " names a child Session", task("running", 2L, null)
|
||||
.put("kind", kind).put("child_session_id",
|
||||
"0d7f3c2e-5b6a-4e1f-8a9b-abcdef012345"));
|
||||
}
|
||||
assertThat(failures).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void taskEventsKeepOneShapePerType() {
|
||||
check("PublicTaskEvent", "state_changed",
|
||||
|
||||
+34
@@ -318,6 +318,25 @@ class SurfaceAdmissionAcceptanceTest {
|
||||
com.alibaba.qwen.code.managedagent.store.ManagedSessionStore
|
||||
.sessionScopeKey(tenant, session),
|
||||
recordKey, tenant, session, "b".repeat(64));
|
||||
// The task view reads a child task's committed body for its child
|
||||
// Session link (#13746 F2), so the list probes need one.
|
||||
String scope = com.alibaba.qwen.code.managedagent.store
|
||||
.ManagedSessionStore.sessionScopeKey(tenant, session);
|
||||
byte[] body = "{\"childSessionId\":null}".getBytes(
|
||||
StandardCharsets.UTF_8);
|
||||
jdbc.update("INSERT INTO ma_resource (resource_key,"
|
||||
+ " session_scope_key, tenant_id, workspace_id,"
|
||||
+ " session_id, resource_id, kind, schema_version,"
|
||||
+ " byte_length, sha256, storage_kind, inline_bytes,"
|
||||
+ " publish_command_id, state, gmt_create) VALUES (?,"
|
||||
+ " ?, ?, 'ws', ?, 'resource-settled',"
|
||||
+ " 'managed-child_run', 1, ?, ?, 'MYSQL_INLINE', ?,"
|
||||
+ " 'command', 'REFERENCED', 1)",
|
||||
com.alibaba.qwen.code.runtimebroker.managedschema
|
||||
.ManagedSchemaKeys.key(scope, "resource-settled"),
|
||||
scope, tenant, session, body.length,
|
||||
org.apache.commons.codec.digest.DigestUtils.sha256Hex(body),
|
||||
body);
|
||||
return "task_" + recordKey;
|
||||
}
|
||||
|
||||
@@ -561,6 +580,21 @@ class SurfaceAdmissionAcceptanceTest {
|
||||
.andExpect(status().isAccepted());
|
||||
}
|
||||
|
||||
// The admitted list probes only prove "not refused": pin that a
|
||||
// reader's task list really serves the child task whose committed body
|
||||
// the view reads for its child Session link (#13746 F2).
|
||||
@Test
|
||||
void aReadersTaskListServesTheChildTaskThroughItsBody() throws Exception {
|
||||
mvc.perform(get("/v1/agents/sessions/" + bound + "/tasks")
|
||||
.header(TenantContextFilter.HEADER, tenant)
|
||||
.principal(actor(tenant, READER)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data[?(@.id == '" + settledTask
|
||||
+ "')].kind").value("child_agent"))
|
||||
.andExpect(jsonPath("$.data[?(@.id == '" + settledTask
|
||||
+ "')].child_session_id").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readerRoutesAnswerTheBoundGrantHolderAndTheLegacyArm()
|
||||
throws Exception {
|
||||
|
||||
@@ -1130,6 +1130,11 @@ export interface components {
|
||||
taskId: string;
|
||||
/** Format: uuid */
|
||||
sessionId: string;
|
||||
/**
|
||||
* Format: uuid
|
||||
* @description The child Session that runs this task: present for a child_agent or workflow task once the run's committed record names it (the child's attach, or a terminal verdict that names a minted child), and absent for every other kind and before then. The child is a Session in the same Workspace, readable by exactly the callers that can read this task, so its Turns, items and events are the task's own output; reading them needs no other grant.
|
||||
*/
|
||||
childSessionId?: string;
|
||||
kind: components["schemas"]["TaskKind"];
|
||||
state: components["schemas"]["TaskState"];
|
||||
/**
|
||||
@@ -1158,7 +1163,7 @@ export interface components {
|
||||
/** @description The newest Artifacts that hold durable task output, at most 100, oldest first. Entries must not rotate out until older Artifacts can be enumerated and attributed to the task: an evicted Artifact stays readable by id through the Session artifact routes but is no longer discoverable from the task. */
|
||||
artifactRefs: string[];
|
||||
actionCapabilities: components["schemas"]["TaskActionCapability"][];
|
||||
} & (unknown & unknown & unknown & unknown);
|
||||
} & (unknown & unknown & unknown & unknown & unknown);
|
||||
WebShellTaskPage: {
|
||||
data: components["schemas"]["WebShellTask"][];
|
||||
hasMore: boolean;
|
||||
|
||||
Reference in new issue
Block a user