fix(agent-team): limit baseline loading fix to the panel
This commit is contained in:
1 parent
bfc772bb47
commit
700b636508
20 files changed
+56
-144
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
|
||||
README.md: e00acab6862e66f1c1f34ce81f0249513c9bf5c8
|
||||
README.zh.md: b2ad89a8664d54f70d2364aeaf3b1c7d3b3f5bff
|
||||
README.md: e12ad7623eefc73c94e99cc3078e0ae78d6a969a
|
||||
README.zh.md: 5f4ceec9a8e8303cad6651dbbe7af27fc07bccec
|
||||
@@ -39,7 +39,7 @@ Client list refreshes retain unchanged row objects and reuse the items array whe
|
||||
|
||||
Background-job rows and observation streams belong to [`dsh-api-job-controller`](../job-controller/README.md); the control stream carries projections only.
|
||||
|
||||
Projection reads retain loading and failure state independently of their values. `refreshProjections` reuses a successful baseline within one connection by default. Passing `{ force: true }` requests a fresh baseline after any running read; concurrent forced calls during that read share one follow-up. Session removal, reconnect, and disposal cancel queued follow-ups. Reconnect cancels reads from the previous connection and reloads previously requested projections; live membership updates arrive through the control stream. Parent availability comes from Host summaries and remains unknown until a summary or a successful list baseline establishes it. Address lookup resolves projected children without selecting them or creating scopes.
|
||||
Projection reads retain loading and failure state independently of their values. Reconnect cancels reads from the previous connection and reloads previously requested projections; live membership updates arrive through the control stream. Parent availability comes from Host summaries and remains unknown until a summary or a successful list baseline establishes it. Address lookup resolves projected children without selecting them or creating scopes.
|
||||
|
||||
Accepted prompts and observed running keep their Client display conversion across list refresh, reconnect, and Session-object replacement. The Manager retains these observations by Session id; they do not establish durable history. Drafts, projection stores, and sidebar display rules retain their existing behavior. The [blank rollback decision](../../../.agents/notes/implemented/bug-fix/2026-09-15-client-session-blank-reconciliation.md) defines retention and late-response handling.
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Client 列表刷新保留未变化的行对象,并在顺序和值均相同时
|
||||
|
||||
后台 job 的名册行与观测流属于 [`dsh-api-job-controller`](../job-controller/README.zh.md);控制流只承载投影。
|
||||
|
||||
投影读取独立于其值保留加载与失败状态。`refreshProjections` 默认复用同一连接内已成功读取的基线。传入 `{ force: true }` 会在当前读取结束后请求新基线;当前读取期间并发的强制刷新调用共用一次后续读取。会话移除、重连和释放会取消排队的后续读取。重连会取消上一连接的读取,并重新加载已请求的投影;实时成员更新通过 control stream 到达。父 Agent 可用性来自 Host 摘要,在收到对应摘要或成功的列表 baseline 前保持未知。地址查找直接解析投影中的子项,不会选择它们或创建 scope。
|
||||
投影读取独立于其值保留加载与失败状态。重连会取消上一连接的读取,并重新加载已请求的投影;实时成员更新通过 control stream 到达。父 Agent 可用性来自 Host 摘要,在收到对应摘要或成功的列表 baseline 前保持未知。地址查找直接解析投影中的子项,不会选择它们或创建 scope。
|
||||
|
||||
已受理的提示词与已观察到的运行,会使客户端展示转换跨列表刷新、重连及 Session 对象替换保留。Manager 按 Session id 保留这些观察;它们不证明持久历史已经产生。草稿、投影存储和侧边栏展示规则保留原有行为。[blank 回退修复决策](../../../.agents/notes/implemented/bug-fix/2026-09-15-client-session-blank-reconciliation.zh.md) 定义保留期限及迟到响应处理。
|
||||
|
||||
|
||||
@@ -21,12 +21,6 @@ export type { AgentContext } from '../scope.ts'
|
||||
/** Known Session identity or durable direct-parent subagent address; an address owns no lifetime. */
|
||||
export type SessionTarget = SessionId | SubagentAddress
|
||||
|
||||
/** Cache policy for an explicit Session projection read. */
|
||||
export interface SessionProjectionRefreshOptions {
|
||||
/** Read again after any current request; concurrent forced reads share that follow-up. */
|
||||
readonly force?: boolean
|
||||
}
|
||||
|
||||
/** One independent use of an exact Client generation, without Host Agent ownership. */
|
||||
export interface SessionReference extends Disposable {
|
||||
readonly sessionId: SessionId
|
||||
@@ -102,12 +96,11 @@ export interface ISessions {
|
||||
subagentAddress(id: SessionId): SubagentAddress | undefined
|
||||
|
||||
/**
|
||||
* Load all Session projections once per connection; retry unsuccessful reads.
|
||||
* Load all Session projections once per connection; retry an unsuccessful initial read.
|
||||
* @param sessionId - Session to inspect without opening its conversation.
|
||||
* @param options - force a fresh read even after a successful baseline; a running read finishes first.
|
||||
* @returns completion of the requested read; removal, reconnect, or disposal cancels a queued refresh.
|
||||
* @returns completion of the current or newly started refresh.
|
||||
*/
|
||||
refreshProjections(sessionId: SessionId, options?: SessionProjectionRefreshOptions): Promise<void>
|
||||
refreshProjections(sessionId: SessionId): Promise<void>
|
||||
|
||||
/**
|
||||
* Refresh the Host-authoritative Session list.
|
||||
|
||||
@@ -50,7 +50,7 @@ export type {
|
||||
SubmissionHandle,
|
||||
} from './contract/session.ts'
|
||||
export type {
|
||||
ISessions, SessionProjectionRefreshOptions, SessionReference, SessionRetainInfo, SessionRetainOptions, SessionTarget,
|
||||
ISessions, SessionReference, SessionRetainInfo, SessionRetainOptions, SessionTarget,
|
||||
} from './contract/sessions.ts'
|
||||
export { MutableSessionEventSource } from './contract/events.ts'
|
||||
export type {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Notifier } from './notifier.ts'
|
||||
import { ProjectionValueStore } from './projection-store.ts'
|
||||
import { Session } from './session.ts'
|
||||
import type { SessionRemotes } from './remotes.ts'
|
||||
import type { SessionProjectionRefreshOptions, SessionTarget } from '../contract/sessions.ts'
|
||||
import type { SessionTarget } from '../contract/sessions.ts'
|
||||
|
||||
function sessionSeqCursor(value: number): SessionSeqCursor {
|
||||
return value === -1 ? -1 : SessionSeq(value)
|
||||
@@ -68,7 +68,6 @@ export interface SessionProjectionSnapshot {
|
||||
interface ProjectionInflight {
|
||||
readonly promise: Promise<void>
|
||||
readonly controller: AbortController
|
||||
refreshAfter?: Promise<void>
|
||||
}
|
||||
|
||||
type ProjectionLoad = Omit<SessionProjectionSnapshot, 'values'>
|
||||
@@ -335,19 +334,12 @@ export class SessionManager {
|
||||
/**
|
||||
* Load a complete projection baseline once per connection; retry unsuccessful reads.
|
||||
* @param sessionId - Session to inspect without opening its conversation.
|
||||
* @param options - force a fresh read after any current request, including a cached successful baseline.
|
||||
* @returns completion of the requested read; aborting the current request also cancels its queued refresh.
|
||||
* @returns completion of the current or newly started read.
|
||||
*/
|
||||
refreshProjections(sessionId: SessionId, options?: SessionProjectionRefreshOptions): Promise<void> {
|
||||
refreshProjections(sessionId: SessionId): Promise<void> {
|
||||
const existing = this.projectionInflight.get(sessionId)
|
||||
if (existing !== undefined) {
|
||||
if (!options?.force) return existing.refreshAfter ?? existing.promise
|
||||
// A running read may predate plugin activation and omit its new projection.
|
||||
return existing.refreshAfter ??= existing.promise.then(() => {
|
||||
if (!existing.controller.signal.aborted) return this.refreshProjections(sessionId, { force: true })
|
||||
})
|
||||
}
|
||||
if (!options?.force && this.projectionLoads.get(sessionId)?.state === 'ready') return Promise.resolve()
|
||||
if (existing !== undefined) return existing.promise
|
||||
if (this.projectionLoads.get(sessionId)?.state === 'ready') return Promise.resolve()
|
||||
const controller = new AbortController()
|
||||
const store = this.projectionStore(sessionId)
|
||||
const initialValues = store.values()
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protoc
|
||||
import type { SessionEventSource } from '../contract/events.ts'
|
||||
import type { SessionFace } from '../contract/session.ts'
|
||||
import type {
|
||||
AgentContext, ISessions, SessionProjectionRefreshOptions, SessionReference, SessionRetainInfo, SessionRetainOptions, SessionTarget,
|
||||
AgentContext, ISessions, SessionReference, SessionRetainInfo, SessionRetainOptions, SessionTarget,
|
||||
} from '../contract/sessions.ts'
|
||||
import type { SessionReferenceSource } from '../index.ts'
|
||||
import { createScope, scopeIdentityOf, scopeOf as scopeTagOf } from '../scope.ts'
|
||||
@@ -339,12 +339,11 @@ export class ClientSessions implements ISessions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all Session projections using the requested cache policy.
|
||||
* Load all Session projections once per connection; retry an unsuccessful initial read.
|
||||
* @param sessionId - Session to inspect without opening its conversation.
|
||||
* @param options - optional fresh read after any current request.
|
||||
*/
|
||||
refreshProjections(sessionId: SessionId, options?: SessionProjectionRefreshOptions): Promise<void> {
|
||||
return this.manager.refreshProjections(sessionId, options)
|
||||
refreshProjections(sessionId: SessionId): Promise<void> {
|
||||
return this.manager.refreshProjections(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -927,63 +927,6 @@ describe('subagent catalogs', () => {
|
||||
expect(remote.session.projections.mock.calls.map(([request]) => request)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('refreshes a successful baseline to discover a newly available projection', async ({ mock, remote }) => {
|
||||
const manager = makeManager(mock, remote)
|
||||
onTestFinished(() => manager.dispose())
|
||||
remote.session.projections.mockResolvedValueOnce(ok({ asOfSeq: 0, values: {} }))
|
||||
await manager.refreshProjections(S1)
|
||||
remote.session.projections.mockResolvedValueOnce(ok({ asOfSeq: 0, values: { title: 'new capability' } }))
|
||||
await manager.refreshProjections(S1, { force: true })
|
||||
expect(manager.getListSnapshot().projectionsBySession[S1]?.values.title).toBe('new capability')
|
||||
await manager.refreshProjections(S1)
|
||||
expect(remote.session.projections).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('coalesces forced reads behind an older request without regressing newer frames', async ({ mock, remote }) => {
|
||||
const manager = makeManager(mock, remote)
|
||||
onTestFinished(() => manager.dispose())
|
||||
const old = Promise.withResolvers<Awaited<ReturnType<typeof remote.session.projections>>>()
|
||||
const fresh = Promise.withResolvers<Awaited<ReturnType<typeof remote.session.projections>>>()
|
||||
remote.session.projections.mockImplementationOnce(() => old.promise)
|
||||
remote.session.projections.mockImplementationOnce(() => fresh.promise)
|
||||
const initial = manager.refreshProjections(S1)
|
||||
const refresh = manager.refreshProjections(S1, { force: true })
|
||||
expect(manager.refreshProjections(S1, { force: true })).toBe(refresh)
|
||||
expect(manager.refreshProjections(S1)).toBe(refresh)
|
||||
expect(remote.session.projections).toHaveBeenCalledOnce()
|
||||
old.resolve(ok({ asOfSeq: 0, values: {} }))
|
||||
await initial
|
||||
expect(remote.session.projections).toHaveBeenCalledTimes(2)
|
||||
manager.handleControlFrame({ type: 'projection', sessionId: S1, key: 'title', seq: 2, value: 'live title' })
|
||||
fresh.resolve(ok({ asOfSeq: 1, values: { title: 'older title', subagentCatalog: [] } }))
|
||||
await refresh
|
||||
expect(manager.getListSnapshot().projectionsBySession[S1]).toMatchObject({
|
||||
state: 'ready', values: { title: 'live title', subagentCatalog: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it.for(['remove', 'reconnect', 'dispose'] as const)(
|
||||
'cancels a queued forced read on %s',
|
||||
async (action, { mock, remote }) => {
|
||||
const manager = makeManager(mock, remote)
|
||||
onTestFinished(() => manager.dispose())
|
||||
const old = Promise.withResolvers<Awaited<ReturnType<typeof remote.session.projections>>>()
|
||||
remote.session.projections.mockImplementationOnce(() => old.promise)
|
||||
remote.session.projections.mockResolvedValue(ok({ asOfSeq: 0, values: { title: 'new connection' } }))
|
||||
const initial = manager.refreshProjections(S1)
|
||||
const refresh = manager.refreshProjections(S1, { force: true })
|
||||
let disposal: Promise<void> | undefined
|
||||
if (action === 'remove') manager.handleSessionRemoved(S1)
|
||||
else if (action === 'reconnect') manager.handleConnected()
|
||||
else disposal = manager.dispose()
|
||||
old.resolve(ok({ asOfSeq: 0, values: { title: 'old connection' } }))
|
||||
await Promise.all([initial, refresh, disposal])
|
||||
expect(remote.session.projections).toHaveBeenCalledTimes(action === 'reconnect' ? 2 : 1)
|
||||
expect(manager.getListSnapshot().projectionsBySession[S1]?.values.title)
|
||||
.toBe(action === 'reconnect' ? 'new connection' : undefined)
|
||||
},
|
||||
)
|
||||
|
||||
it('uses current Session status rather than the time an initial catalog read started', async ({ mock, remote }) => {
|
||||
const response = Promise.withResolvers<Awaited<ReturnType<typeof remote.session.projections>>>()
|
||||
remote.session.projections.mockImplementation(() => response.promise)
|
||||
|
||||
@@ -719,17 +719,6 @@ describe('borrow-only bindings', () => {
|
||||
})
|
||||
|
||||
describe('catalog-addressed navigation', () => {
|
||||
it('refreshes cached projection baselines through the public Session service', async ({ bench }) => {
|
||||
const b = bench()
|
||||
b.mock.remote.session.projections.mockResolvedValueOnce(ok({ asOfSeq: 0, values: {} }))
|
||||
await b.svc.refreshProjections(sid('root'))
|
||||
b.mock.remote.session.projections.mockResolvedValueOnce(ok({ asOfSeq: 0, values: { title: 'new capability' } }))
|
||||
await b.svc.refreshProjections(sid('root'), { force: true })
|
||||
await b.svc.refreshProjections(sid('root'))
|
||||
expect(b.svc.list.getSnapshot().projectionsBySession[sid('root')]?.values.title).toBe('new capability')
|
||||
expect(b.mock.remote.session.projections).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('retains a projected child independently of its parent and shares its history generation', async ({ bench }) => {
|
||||
const b = bench()
|
||||
await feedList(b, [{ id: 'root' }])
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/experimental/agent-team/README.md
|
||||
README.md: 79fc15fa369ec4615d3a3531de7c1fd54aa41ee7
|
||||
README.zh.md: 8e1905ded14b8cb803298c470085b6ac2a426fd4
|
||||
README.md: 9c0a351cf6017bb026b864c643a2ebeaa19ccbdc
|
||||
README.zh.md: 44a3ade10100d7d44d9ed9735cedfc75684fa2ae
|
||||
@@ -176,7 +176,7 @@ Read these pages when the package-level contract is not enough. They move from t
|
||||
|
||||
The `agentTeam` Session projection publishes the Lead Session's durable roster, non-deleted task views, and any `failure` beside the last valid state. Its `apply` replaces only the touched collection; mailbox-only changes retain the client view reference and produce no frame. The [subsystem reference](../../../docs/subsystems/agent-team.md#web-projection) defines the wire types.
|
||||
|
||||
The Web UI reads current-session values through the standard projection hook and overlays activity from Session status. Opening the panel refreshes the Lead baseline; other active members' baselines are loaded for durable model selection without opening their conversations. Task creation and updates belong to Team agents through the service and model tools. The `./client` export supplies browser-safe roster, task, and projection types.
|
||||
The Web UI reads current-session values through the standard projection hook and overlays activity from Session status. The current Session supplies its baseline through conversation loading; other active members' baselines are loaded for durable model selection without opening their conversations. Task creation and updates belong to Team agents through the service and model tools. The `./client` export supplies browser-safe roster, task, and projection types.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ dispose 会关闭准入、中止并等待已获准的创建与 mailbox dispatch
|
||||
|
||||
`agentTeam` Session 投影发布 Lead Session 的持久 roster、未删除任务视图,以及最后有效状态旁的 `failure`。其 `apply` 只替换被触及的集合;仅邮箱的变化保留客户端视图引用,不产生 frame。[子系统参考](../../../docs/subsystems/agent-team.zh.md#web-projection) 定义传输类型。
|
||||
|
||||
Web UI 通过标准投影钩子读取当前会话的值,并从 Session 状态叠加活动信息。打开 panel 时会刷新 Lead 基线;其他 active 成员的基线用于读取持久模型选择,无需打开它们的会话。任务创建与更新由 Team agent 通过服务和模型工具完成。`./client` 导出可供浏览器使用的 roster、任务与投影类型。
|
||||
Web UI 通过标准投影钩子读取当前会话的值,并从 Session 状态叠加活动信息。当前 Session 通过会话加载提供基线;其他 active 成员的基线用于读取持久模型选择,无需打开它们的会话。任务创建与更新由 Team agent 通过服务和模型工具完成。`./client` 导出可供浏览器使用的 roster、任务与投影类型。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/experimental/client-ui-agent-team/README.md
|
||||
README.md: 8a94b57035d3ed56c2b88b91020eff659384fabe
|
||||
README.zh.md: cbea68e5aa91ccc27bbcc9f3a89beed2bc2d5580
|
||||
README.md: b6930b9cc893cbdf1a9734c8e81c260d23fabbb3
|
||||
README.zh.md: 93f9a0d036947bda994298967bc3574e136ce9f4
|
||||
@@ -29,7 +29,7 @@ Enable this package through [`@deepseek-ai/dsh-experimental-agent-team-profile`]
|
||||
|
||||
### Inspect and navigate the roster
|
||||
|
||||
The trigger shows the teammate count, and the panel shows the roster and task board of the Lead Session's `agentTeam` projection, so a task created by an agent or a teammate reaching `active` appears while the panel is open. Opening the panel refreshes the Lead's projection baseline, including when the Team plugin was enabled after the conversation opened. A read already in progress finishes before this refresh starts. Failed reads show a retry control alongside any last valid Team; a reconnect reloads previously requested projections. A successful read without the Team capability shows an unavailable notice. The panel also requests active members' projection baselines outside the current Session and Lead, including persisted Sessions that have not been opened, and loads new members when they become active. Roster rows show durable names and a status: `failed` and `provisioning` come from the durable member phase, and `running` or `inactive` comes from the member Session's live status. A model appears when the member Session's `modelSelection` projection records a durable selection or request. Provisioning and running members use the shared ongoing loader, inactive members use idle, and failed members use error. Selecting a healthy teammate opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address from its Lead and roster identity without refreshing or checking the parent catalog. The Host validates the parent, child, and mode when history opens. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific address field.
|
||||
The trigger shows the teammate count, and the panel shows the roster and task board of the Lead Session's `agentTeam` projection, so a task created by an agent or a teammate reaching `active` appears while the panel is open. Opening the panel requests the Lead's projection baseline once per connection, both in the Lead conversation and in a teammate conversation. A successful prior read is reused; a failed read shows a retry control alongside any last valid Team, and a reconnect reloads it. A successful read without the Team capability shows an unavailable notice. The panel also requests active members' projection baselines outside the current Session and Lead, including persisted Sessions that have not been opened, and loads new members when they become active. Roster rows show durable names and a status: `failed` and `provisioning` come from the durable member phase, and `running` or `inactive` comes from the member Session's live status. A model appears when the member Session's `modelSelection` projection records a durable selection or request. Provisioning and running members use the shared ongoing loader, inactive members use idle, and failed members use error. Selecting a healthy teammate opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address from its Lead and roster identity without refreshing or checking the parent catalog. The Host validates the parent, child, and mode when history opens. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific address field.
|
||||
|
||||
### Inspect the task board
|
||||
|
||||
@@ -47,7 +47,7 @@ The read-only task board shows task identity, owner, blockers, readiness, adviso
|
||||
|
||||
The Client export registers its locale dictionaries and one conversation-header slot through Cordis effects; it mounts no Remote namespace. Disposing the plugin fiber removes both registrations.
|
||||
|
||||
The panel renders outside the conversation container and stays within the viewport. Opening moves focus into the panel; Escape or Close returns focus to its trigger. Clicking outside or moving focus outside the panel and trigger closes it without moving focus back. The component reads the current Session's Team through `useProjection('agentTeam')` and its model through `useProjection('modelSelection')`. In a teammate conversation, `useSession` supplies the Lead identity and `useSessions` selects the parent's Team value. Separate `useSessions` selectors read the Lead's baseline-read state and error in either conversation. Each roster row selects its own model and running state; the panel subscribes to neither the complete projection collection nor the complete summary or status collection. Its only injected callbacks read a Session's projection baseline and open a teammate. Switching conversations closes the panel and clears a navigation failure.
|
||||
The panel renders outside the conversation container and stays within the viewport. Opening moves focus into the panel; Escape or Close returns focus to its trigger. Clicking outside or moving focus outside the panel and trigger closes it without moving focus back. The component reads the current Session's Team through `useProjection('agentTeam')` and its model through `useProjection('modelSelection')`. In a teammate conversation, `useSession` supplies the Lead identity and `useSessions` reads its Team value. Separate `useSessions` selectors read the Lead's baseline-read state and error in both conversation types. Each roster row selects its own model and running state; the panel subscribes to neither the complete projection collection nor the complete summary or status collection. Its only injected callbacks read a Session's projection baseline and open a teammate. Switching conversations closes the panel and clears a navigation failure.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
@@ -64,7 +64,7 @@ The panel renders outside the conversation container and stays within the viewpo
|
||||
## Further Exploration
|
||||
|
||||
- [Agent Teams bundle](../agent-team-profile/README.md) — the published opt-in bundle that mounts this Client plugin.
|
||||
- [Agent Teams service](../agent-team/README.md) — authoritative roster, task, and projection behavior.
|
||||
- [Agent Teams service](../agent-team/README.md) — authoritative roster, task, projection, and Remote behavior.
|
||||
- [Conversation UI](../../client/ui-conversation/README.md) — the stable header slot and addressed-subagent navigation surface.
|
||||
- [Experimental packages](../README.md) — incubation status and publication policy.
|
||||
|
||||
@@ -83,6 +83,7 @@ No direct effect; the Team tools and ordinary conversation submission own any la
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **Cached capability absence** — if another UI loaded the Lead projection baseline before Agent Teams was enabled, opening this panel reuses that baseline; reconnect to load the newly enabled capability.
|
||||
- **No mailbox timeline** — the projection view carries roster and tasks only; peer messages are not shown.
|
||||
- **Model before the first request** — a member without a durable model selection or request shows no model. Inactive persisted members can supply their model through the explicit projection read; live activity still requires a running Host agent.
|
||||
- **Ordinary child continuation** — a human message sent after navigation uses the stable addressed-subagent prompt path, not the Team peer mailbox.
|
||||
|
||||
@@ -29,7 +29,7 @@ kind: "package-reference"
|
||||
|
||||
### 检查并导航 roster
|
||||
|
||||
触发按钮显示 teammate 数量,panel 显示 Lead Session `agentTeam` 投影中的 roster 与任务板,因此 agent 创建的任务或进入 `active` 的 teammate 会在 panel 打开期间直接出现。打开 panel 时会刷新 Lead 的投影基线,包括在会话打开后才启用 Team 插件的情况。若已有读取正在进行,本次刷新会在其结束后开始。读取失败时,最后有效 Team 旁会显示重试控件;重连会重新加载已请求的投影。读取成功但缺少 Team 能力时显示不可用提示。panel 还会请求当前 Session 与 Lead 之外 active 成员的投影基线,包括尚未打开的持久 Session,并在新成员进入 active 时加载它。Roster row 展示持久 name 与状态:`failed` 与 `provisioning` 来自持久成员 phase,`running` 或 `inactive` 来自成员 Session 的实时状态。当成员 Session 的 `modelSelection` 投影记录了持久选择或请求时显示 model。provisioning 和 running 成员使用共享 ongoing loading,inactive 成员使用 idle 灰点,failed 成员使用 error 红点。选择健康 teammate 时,系统直接根据其 Lead 与 roster 身份打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address,不刷新或检查 parent catalog。Host 在打开历史时校验 parent、child 与 mode。History 与后续人类提示词继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。
|
||||
触发按钮显示 teammate 数量,panel 显示 Lead Session `agentTeam` 投影中的 roster 与任务板,因此 agent 创建的任务或进入 `active` 的 teammate 会在 panel 打开期间直接出现。无论在 Lead 会话还是 teammate 会话中,打开 panel 时都按每次连接一次的规则请求 Lead 的投影基线。之前读取成功则复用;读取失败会在最后有效 Team 旁显示重试控件,重连后会重新加载。读取成功但缺少 Team 能力时显示不可用提示。panel 还会请求当前 Session 与 Lead 之外 active 成员的投影基线,包括尚未打开的持久 Session,并在新成员进入 active 时加载它。Roster row 展示持久 name 与状态:`failed` 与 `provisioning` 来自持久成员 phase,`running` 或 `inactive` 来自成员 Session 的实时状态。当成员 Session 的 `modelSelection` 投影记录了持久选择或请求时显示 model。provisioning 和 running 成员使用共享 ongoing loading,inactive 成员使用 idle 灰点,failed 成员使用 error 红点。选择健康 teammate 时,系统直接根据其 Lead 与 roster 身份打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address,不刷新或检查 parent catalog。Host 在打开历史时校验 parent、child 与 mode。History 与后续人类提示词继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。
|
||||
|
||||
### 查看任务板
|
||||
|
||||
@@ -47,7 +47,7 @@ kind: "package-reference"
|
||||
|
||||
Client export 通过 Cordis effect 注册 locale dictionary 与一个 conversation-header slot;它不挂载任何 Remote namespace。Dispose plugin fiber 会移除这两项 registration。
|
||||
|
||||
面板渲染在会话容器外,并保持在视口范围内。打开时焦点移入面板;按 Escape 或选择关闭按钮时,焦点返回触发按钮。点击外部或将焦点移出面板与触发按钮时,面板关闭,但不会将焦点移回。组件通过 `useProjection('agentTeam')` 读取当前 Session 的 Team,通过 `useProjection('modelSelection')` 读取其 model。在 teammate 会话中,`useSession` 提供 Lead 身份,`useSessions` 选择父会话的 Team 值。两种会话都通过独立的 `useSessions` selector 读取 Lead 的基线读取状态和错误。每个 roster row 只选择自己的 model 和运行状态;panel 不订阅完整投影集合,也不订阅完整摘要或状态集合。它仅有的注入回调是读取 Session 的投影基线与打开 teammate。切换会话会关闭面板并清除导航失败。
|
||||
面板渲染在会话容器外,并保持在视口范围内。打开时焦点移入面板;按 Escape 或选择关闭按钮时,焦点返回触发按钮。点击外部或将焦点移出面板与触发按钮时,面板关闭,但不会将焦点移回。组件通过 `useProjection('agentTeam')` 读取当前 Session 的 Team,通过 `useProjection('modelSelection')` 读取其 model。在 teammate 会话中,`useSession` 提供 Lead 身份,`useSessions` 读取其 Team 值。两类会话均通过独立的 `useSessions` selector 读取 Lead 的基线读取状态和错误。每个 roster row 只选择自己的 model 和运行状态;panel 不订阅完整投影集合,也不订阅完整摘要或状态集合。它仅有的注入回调是读取 Session 的投影基线与打开 teammate。切换会话会关闭面板并清除导航失败。
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
@@ -64,7 +64,7 @@ Client export 通过 Cordis effect 注册 locale dictionary 与一个 conversati
|
||||
## 进一步探索
|
||||
|
||||
- [Agent Teams bundle](../agent-team-profile/README.zh.md)——挂载本 Client plugin 的公开 opt-in bundle。
|
||||
- [Agent Teams service](../agent-team/README.zh.md)——权威 roster、task 与投影行为。
|
||||
- [Agent Teams service](../agent-team/README.zh.md)——权威 roster、task、投影与 Remote 行为。
|
||||
- [会话 UI](../../client/ui-conversation/README.zh.md)——稳定 header slot 与 addressed-subagent 导航表层。
|
||||
- [实验性包](../README.zh.md)——孵化状态与发布规则。
|
||||
|
||||
@@ -83,6 +83,7 @@ Client export 通过 Cordis effect 注册 locale dictionary 与一个 conversati
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **能力缺失的缓存** — 如果其他 UI 在启用 Agent Teams 前已读取 Lead 的投影基线,打开本面板会复用该基线;重新连接后才能加载新启用的能力。
|
||||
- **没有 mailbox timeline**——投影视图只承载 roster 与任务;不显示 peer 消息。
|
||||
- **首次请求前的模型** — 没有持久模型选择或请求的成员不显示模型。未运行的持久成员可通过显式投影读取提供模型;实时活动仍需要正在运行的 Host agent。
|
||||
- **普通 child continuation**——导航后发送的人类消息使用稳定 addressed-subagent 提示词路径,而不是 Team peer mailbox。
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
TeamMemberProjection,
|
||||
TeamTaskView as TeamTask,
|
||||
} from '@deepseek-ai/dsh-experimental-agent-team/client'
|
||||
import type { SessionProjectionRefreshOptions } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type {} from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import {
|
||||
IconCloseOutlineRegular, IconRefreshOutlineRegular, IconUserOutlineRegular, StateDot,
|
||||
useAnchoredPosition, useDismissOnOutsidePointer, type StateDotState,
|
||||
@@ -17,8 +17,8 @@ import css from './TeamAction.module.css'
|
||||
|
||||
/** Business actions injected by the browser plugin. */
|
||||
export interface TeamActionInjected {
|
||||
/** Read a Session's baseline, optionally refreshing after any existing read. */
|
||||
loadProjections: (sessionId: SessionId, options?: SessionProjectionRefreshOptions) => void
|
||||
/** Read a Session's projection baseline once per connection, or retry a failed read. */
|
||||
loadProjections: (sessionId: SessionId) => void
|
||||
openTeammate: (sessionId: SessionId, member: TeamMemberProjection) => void
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export function TeamAction({
|
||||
}, [sessionId])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) loadProjections(leadSessionId, { force: true })
|
||||
if (open) loadProjections(leadSessionId)
|
||||
}, [open, leadSessionId, loadProjections])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -219,7 +219,7 @@ export function TeamAction({
|
||||
<div className={css.error} role="alert">
|
||||
<StateDot state="error" />
|
||||
<span className={css.spacer}>{failureText(readError)}</span>
|
||||
<button type="button" className={css.iconButton} aria-label={t('retry')} onClick={() => { loadProjections(leadSessionId, { force: true }) }}>
|
||||
<button type="button" className={css.iconButton} aria-label={t('retry')} onClick={() => { loadProjections(leadSessionId) }}>
|
||||
<IconRefreshOutlineRegular size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -36,8 +36,8 @@ export function registerAgentTeamUi(ctx: ClientContext): void {
|
||||
}
|
||||
|
||||
const actions: TeamActionInjected = {
|
||||
loadProjections(sessionId, options): void {
|
||||
void sessions.refreshProjections(sessionId, options)
|
||||
loadProjections(sessionId: SessionId): void {
|
||||
void sessions.refreshProjections(sessionId)
|
||||
},
|
||||
openTeammate(sessionId: SessionId, member: TeamMemberProjection): void {
|
||||
if (member.role !== 'teammate') return
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SessionProjectionRefreshOptions } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
@@ -30,8 +29,8 @@ async function bench(options: { addressed?: boolean } = {}) {
|
||||
},
|
||||
}) } }
|
||||
: undefined,
|
||||
refreshProjections: (id: SessionId, options?: SessionProjectionRefreshOptions) => {
|
||||
navigation.push(['refresh', id, options])
|
||||
refreshProjections: (id: SessionId) => {
|
||||
navigation.push(['refresh', id])
|
||||
return Promise.resolve()
|
||||
},
|
||||
retainInfo: (id: SessionId) => ({
|
||||
@@ -89,10 +88,10 @@ describe('ui-team browser plugin', () => {
|
||||
const t = b.ctx.locale.bind('agent-team')
|
||||
expect(t('trigger')).toBe('Agent Team')
|
||||
|
||||
b.actions().loadProjections(SESSION, { force: true })
|
||||
expect(b.navigation).toEqual([['refresh', SESSION, { force: true }]])
|
||||
b.actions().loadProjections(SESSION)
|
||||
expect(b.navigation).toEqual([['refresh', SESSION]])
|
||||
b.actions().openTeammate(SESSION, LEAD)
|
||||
expect(b.navigation).toEqual([['refresh', SESSION, { force: true }]])
|
||||
expect(b.navigation).toEqual([['refresh', SESSION]])
|
||||
|
||||
await b.fiber.dispose()
|
||||
expect(b.entry()).toBeUndefined()
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('TeamAction', () => {
|
||||
openPanel()
|
||||
expect(await screen.findByText('Implement runtime')).toBeTruthy()
|
||||
expect(screen.getByText('write scopes overlap with task-2')).toBeTruthy()
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledWith(SESSION, { force: true })
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledWith(SESSION)
|
||||
expect(screen.queryByRole('button', { name: /刷新|Refresh/u })).toBeNull()
|
||||
|
||||
setProjection(b.sessions, SESSION, {
|
||||
@@ -192,7 +192,7 @@ describe('TeamAction', () => {
|
||||
render(<TeamAction {...b.props} />)
|
||||
openPanel()
|
||||
expect(screen.getByText('Implement runtime')).toBeTruthy()
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledWith(SESSION, { force: true })
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledWith(SESSION)
|
||||
fireEvent.click(screen.getByRole('button', { name: /^worker/u }))
|
||||
expect(b.injected.openTeammate).toHaveBeenCalledWith(WORKER, worker)
|
||||
})
|
||||
@@ -217,13 +217,13 @@ describe('TeamAction', () => {
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
})
|
||||
|
||||
it('refreshes an already-open Lead on every panel opening and retries failed reads', () => {
|
||||
it('loads projections for an already-open Lead and retries failed reads', () => {
|
||||
const b = bench({ projections: {}, openState: 'open' })
|
||||
render(<TeamAction {...b.props} />)
|
||||
expect(b.injected.loadProjections).not.toHaveBeenCalled()
|
||||
openPanel()
|
||||
expect(screen.getByRole('status').textContent).toBe(zh.loading)
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledExactlyOnceWith(SESSION, { force: true })
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledExactlyOnceWith(SESSION)
|
||||
|
||||
setProjectionSnapshot(b.sessions, SESSION, {
|
||||
state: 'error', error: new RemoteError('gateway/internal', 'offline', {}), values: {},
|
||||
@@ -231,7 +231,7 @@ describe('TeamAction', () => {
|
||||
expect(screen.getByRole('alert').textContent).toBe('offline (gateway/internal)')
|
||||
fireEvent.click(screen.getByRole('button', { name: zh.retry }))
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledTimes(2)
|
||||
expect(b.injected.loadProjections).toHaveBeenLastCalledWith(SESSION, { force: true })
|
||||
expect(b.injected.loadProjections).toHaveBeenLastCalledWith(SESSION)
|
||||
|
||||
setProjectionSnapshot(b.sessions, SESSION, { state: 'loading', error: null, values: {} })
|
||||
expect(screen.getByRole('status').textContent).toBe(zh.loading)
|
||||
@@ -244,7 +244,7 @@ describe('TeamAction', () => {
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledTimes(2)
|
||||
openPanel()
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledTimes(3)
|
||||
expect(b.injected.loadProjections).toHaveBeenLastCalledWith(SESSION, { force: true })
|
||||
expect(b.injected.loadProjections).toHaveBeenLastCalledWith(SESSION)
|
||||
})
|
||||
|
||||
it.each([false, true])('ignores unrelated Session updates (teammate page: %s)', (addressed) => {
|
||||
@@ -277,8 +277,7 @@ describe('TeamAction', () => {
|
||||
expect(screen.getByRole('button', { name: /worker.*updated-worker-model/u })).toBeTruthy()
|
||||
setProjection(b.sessions, SESSION, { ...team, tasks: [{ ...task, subject: 'Updated parent task' }] })
|
||||
expect(screen.getByText('Updated parent task')).toBeTruthy()
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledWith(SESSION, { force: true })
|
||||
expect(b.injected.loadProjections).not.toHaveBeenCalledWith(WORKER, { force: true })
|
||||
expect(b.injected.loadProjections).toHaveBeenCalledWith(SESSION)
|
||||
})
|
||||
|
||||
it('shows capability absence after a successful read instead of loading forever', () => {
|
||||
@@ -299,7 +298,7 @@ describe('TeamAction', () => {
|
||||
expect(screen.getByText('Implement runtime')).toBeTruthy()
|
||||
expect(screen.getByRole('alert').textContent).toBe('offline (gateway/internal)')
|
||||
fireEvent.click(screen.getByRole('button', { name: zh.retry }))
|
||||
expect(b.injected.loadProjections).toHaveBeenLastCalledWith(SESSION, { force: true })
|
||||
expect(b.injected.loadProjections).toHaveBeenLastCalledWith(SESSION)
|
||||
})
|
||||
|
||||
it('loads active members models without opening their conversations', () => {
|
||||
@@ -397,7 +396,7 @@ describe('TeamAction', () => {
|
||||
openPanel()
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
expect(screen.getByRole('status').textContent).toBe(zh.loading)
|
||||
expect(next.injected.loadProjections).toHaveBeenCalledWith('next-lead', { force: true })
|
||||
expect(next.injected.loadProjections).toHaveBeenCalledWith('next-lead')
|
||||
})
|
||||
|
||||
it('keeps panel interactions open and dismisses on outside pointer or Escape', () => {
|
||||
|
||||
@@ -201,10 +201,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
returns: 'a stable read-only source across same-id generations, with zero counts when none is live.',
|
||||
},
|
||||
{
|
||||
signature: 'refreshProjections(sessionId: SessionId, options?: SessionProjectionRefreshOptions): Promise<void>',
|
||||
description: 'Load all Session projections once per connection; retry unsuccessful reads.',
|
||||
parameters: [{ name: 'sessionId', description: 'Session to inspect without opening its conversation.' }, { name: 'options', description: 'force a fresh read even after a successful baseline; a running read finishes first.' }],
|
||||
returns: 'completion of the requested read; removal, reconnect, or disposal cancels a queued refresh.',
|
||||
signature: 'refreshProjections(sessionId: SessionId): Promise<void>',
|
||||
description: 'Load all Session projections once per connection; retry an unsuccessful initial read.',
|
||||
parameters: [{ name: 'sessionId', description: 'Session to inspect without opening its conversation.' }],
|
||||
returns: 'completion of the current or newly started refresh.',
|
||||
},
|
||||
{
|
||||
signature: 'search( query: string, signal: AbortSignal, ): Promise<RemoteResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>>',
|
||||
@@ -871,10 +871,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionMaybeStandardProps',
|
||||
declaration: 'export interface SessionMaybeStandardProps {\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionRefreshOptions',
|
||||
declaration: 'export interface SessionProjectionRefreshOptions {\n readonly force?: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionProviderComponent',
|
||||
declaration: 'export type SessionProviderComponent = (props: SessionAreaProps) => ReactNode;',
|
||||
|
||||
Reference in new issue
Block a user