mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(client): materialize Agent scopes before list baseline
This commit is contained in:
+3
-3
@@ -258,11 +258,11 @@ WebSocket JSON 与进程内 carrier 的入口都从 `unknown` 开始按 `type`
|
||||
|
||||
Host 只投影 request 一级的 `agent` 与 `signal`:`agent` 变为 frame 的一级 `agentId`,`signal` 成为 delivery lifetime,其余字段必须整体为无损 JSON。
|
||||
|
||||
Client 用 `agentId` 同步解析已存在的 Agent Context,把当前 delivery signal 放回 request 的直接 `signal` 字段,再在目标 Context 的私有 key 上调用 Cordis `waterfall()`。
|
||||
Client 用 `agentId` 同步解析或物化 Agent Context,把当前 delivery signal 放回 request 的直接 `signal` 字段,再在目标 Context 的私有 key 上调用 Cordis `waterfall()`。Session-backed adapter 在首个成功 Session 列表 baseline 到达前允许 transport 先物化 scope;baseline 到达后由列表生命周期接管 scope 存活判断。
|
||||
|
||||
系统不扫描任意深度对象,不传 path array 或 placeholder,不 deep clone/restore Context 和 AbortSignal,也不等待未来出现的 Agent Context。
|
||||
|
||||
Client adapter 未注册、Agent Context 不存在或已经释放时,本 Client 立即返回 `next`。它不订阅 registry、不做 resolve 后竞态复查,也不为一次 delivery 创建临时 Fiber。
|
||||
Client adapter 未注册、resolver 未返回 Context 或解析抛错时,本 Client 立即返回 `next`。它不订阅 registry、不做 resolve 后竞态复查,也不为一次 delivery 创建临时 Fiber。
|
||||
|
||||
Gateway Host 为每个未完成 waterfall 保存 `eventId`、Host continuation 与已投递 Client generation。新 Client generation 会收到同一 pending event 的重放。
|
||||
|
||||
@@ -310,7 +310,7 @@ API Proxy 只承接自身拥有的独立业务 API,不是 Session、Workspace
|
||||
|
||||
**把 Agent scope 做成任意深度对象投影。** 递归扫描 Context 与 AbortSignal 需要 path、placeholder、clone 和 restore 协议,并把偶然对象结构升级成 wire 约定;一级 `agent` 与 `signal` 足以覆盖当前 waterfall。
|
||||
|
||||
**等待 Client Agent Context 或 adapter 后再分发。** registry waiter、竞态复查和临时 delivery Fiber 会为一个可直接委托的 Client 增加额外生命周期;目标不存在时立即 `next` 保持 Cordis waterfall 语义。
|
||||
**等待 Client Agent Context 或 adapter 后再分发。** registry waiter、竞态复查和临时 delivery Fiber 会为一个可同步解析或物化目标的 Client 增加额外生命周期;resolver 当下不能提供目标时立即 `next` 保持 Cordis waterfall 语义。
|
||||
|
||||
**给 Remote Event 使用独立物理 WebSocket 或 duplex stream。** Gateway mux 已提供认证升级、复用、取消、错误映射和重连;下行 `$events` 加上 HTTP `$events/result` 足以表达 request/response,不需要第三条连接。
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ export function apply(ctx: Context): void {
|
||||
sessionControl.start()
|
||||
ctx.typert.contexts.registerClient('agent', {
|
||||
identity: candidate => sessions.scopeOf(candidate),
|
||||
resolve: sessionId => sessions.scope(sessionId),
|
||||
resolve: sessionId => sessions.resolveAgentScope(sessionId),
|
||||
})
|
||||
const workspaceModel = new ClientWorkspaceModel(ctx.remote.workspace)
|
||||
const workspaces = new WorkspaceRuntime(ctx, connection.api, workspaceModel, sessions)
|
||||
|
||||
@@ -572,6 +572,18 @@ export class SessionRuntime implements ISessions {
|
||||
return this.resolve(id)?.ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize the Agent scope named by a validated Host Remote Event.
|
||||
* The first successful Session-list baseline becomes authoritative for its
|
||||
* lifetime; until then, transport streams may address the scope in either
|
||||
* arrival order.
|
||||
* @param id - Host-projected Agent identity (the matching Session id).
|
||||
* @returns the identity-stable Agent Context.
|
||||
*/
|
||||
resolveAgentScope(id: SessionId): AgentContext {
|
||||
return (this.scopes.get(id) ?? this.materializeScope(id)).ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the Agent scope tag off a context. Service-method boundary: fetch
|
||||
* bundles must reach scope resolution through ctx.sessions — a cross-bundle
|
||||
@@ -664,6 +676,11 @@ export class SessionRuntime implements ISessions {
|
||||
const existing = this.scopes.get(id)
|
||||
if (existing !== undefined) return existing
|
||||
if (!this.eligible(id)) return undefined
|
||||
return this.materializeScope(id)
|
||||
}
|
||||
|
||||
/** Materialize one scope after its caller establishes that the id may be addressed. */
|
||||
private materializeScope(id: SessionId): ScopeRecord {
|
||||
const { fiber, ctx } = createScope(this.rootCtx, id)
|
||||
const session = this.manager.get(id)
|
||||
// The Session owns its scoped dispatch point (host Agent.loopCtx mirror);
|
||||
@@ -764,6 +781,7 @@ export class SessionRuntime implements ISessions {
|
||||
|
||||
/** Tear down scope + instance for no-longer-eligible sessions off stage; the staged one defers until the stage moves. */
|
||||
private pruneScopes(): void {
|
||||
if (this.list.getSnapshot().phase === 'pending') return
|
||||
for (const [id, record] of this.scopes) {
|
||||
if (this.eligible(id)) continue
|
||||
if (id === this.watched) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SESSION_SEARCH_RESULT_LIMIT } from '@deepseek-ai/dsh-api-session-contro
|
||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||
import * as RuntimeClient from '../src/client/index.ts'
|
||||
import type { ConversationNodeDefinition } from '../src/client/contract/conversation.ts'
|
||||
import { scopeOf } from '../src/client/agents/scope.ts'
|
||||
import { Session } from '../src/client/sessions/session.ts'
|
||||
import { SessionRuntime } from '../src/client/sessions/service.ts'
|
||||
import { FakeApiClient, fakeRemote, ok } from './fake-api.client.ts'
|
||||
@@ -68,6 +69,16 @@ async function flushMicrotasks(): Promise<void> {
|
||||
}
|
||||
|
||||
describe('runtime client apply', () => {
|
||||
it('materializes Host-addressed Agent scopes before the Session list arrives', async () => {
|
||||
const bench = await mount()
|
||||
const adapter = bench.ctx.typert.contexts.getClient('agent')
|
||||
const first = adapter?.resolve('s-early')
|
||||
|
||||
expect(first).toBeDefined()
|
||||
expect(scopeOf(first as Context)).toBe('s-early')
|
||||
expect(adapter?.resolve('s-early')).toBe(first)
|
||||
})
|
||||
|
||||
it('refreshes Sessions on every Gateway connection generation', async () => {
|
||||
const refresh = vi.spyOn(SessionRuntime.prototype, 'handleConnected')
|
||||
const bench = await mount()
|
||||
|
||||
@@ -121,6 +121,22 @@ describe('search', () => {
|
||||
})
|
||||
|
||||
describe('scope tree', () => {
|
||||
it('retains a Host-addressed scope until the first Session baseline owns pruning', async () => {
|
||||
const b = bench()
|
||||
const scoped = b.svc.resolveAgentScope(sid('s-early'))
|
||||
expect(scopeOf(scoped)).toBe('s-early')
|
||||
|
||||
b.svc.handleControlFrame({
|
||||
type: 'baseline',
|
||||
value: { queues: {}, jobs: {}, projections: {} },
|
||||
})
|
||||
await Promise.resolve()
|
||||
expect(b.svc.resolveAgentScope(sid('s-early'))).toBe(scoped)
|
||||
|
||||
await feedList(b, [])
|
||||
expect(b.svc.scope(sid('s-early'))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('mints lazily on first resolution, tags the ctx, and keeps binding identity stable', async () => {
|
||||
const b = bench()
|
||||
await feedList(b, [{ id: 's1' }])
|
||||
|
||||
Reference in New Issue
Block a user