show subagent usage and active duration

This commit is contained in:
kingwl
2026-08-02 23:31:24 +08:00
parent baf133d134
commit b30686d634
35 changed files with 438 additions and 88 deletions

View File

@@ -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/subagent/subagent/README.md
README.md: 9aea27a0f150d90a41d9a7cb4cd422a75e6107fe
README.zh.md: 3f0b534deae53b8d5aff2765974050f26b931953
README.md: ec4af55bcd9374b1abb55d7bb098eef568449684
README.zh.md: 8323853ff0de2a15da6475fc1433a68f0074ee93

View File

@@ -92,6 +92,8 @@ Provider additions and removals also emit `subagent/provider-added` and `subagen
Continuable children do not create `SubagentRun` or Tasks. The continuation manager directly owns one process-local Activation and retained `AgentHandle` per resident child Session, uses the Agent inbox as the only FIFO, and cold-resumes from the durable descriptor. Exact live direct-parent identity authorizes parent-to-child delivery. Exact live child identity authorizes reports; the manager derives the recipient from durable `parentSession`, and `MessageSource` remains provenance rather than authority.
When `ctx.sessionProjections` is available, the service registers `subagentTiming`. The projection resets at each descriptor so a fork seed's ancestor work cannot enter the child's total, then accumulates `turn/start``turn/end` active time and retains `activeSince` for an open turn. Only descriptors and turn boundaries change the value, so token chunks do not create timing updates.
`registerContinuableSetup()` lets optional packages add child-scoped capabilities without teaching the continuation manager their names. Contributions install synchronously before Activation publication, roll back with failed setup, and are released with the child scope. New grants wait for the next Activation, while contribution removal revokes every resident installation immediately.
## Collection model

View File

@@ -92,6 +92,8 @@ subagent seam 允许一个 agent智能体通过具名提供方把工作委
可继续子级不会创建 `SubagentRun` 或 Task。延续管理器为每个驻留子 Session 直接拥有一个仅存在于当前进程的 Activation 和一个留存的 `AgentHandle`,使用 Agent inbox 作为唯一 FIFO并从持久化描述符冷恢复。父到子投递由准确的实时直接父级身份授权。上报则由准确的实时子级身份授权管理器根据持久化的 `parentSession` 推导接收方,`MessageSource` 仍只表示来源,不表示权限。
`ctx.sessionProjections` 可用时,服务会注册 `subagentTiming`。该投影会在每个描述符处重置,使 fork 种子中的祖先工作不会计入 child 总量,随后累加 `turn/start``turn/end` 活跃时间,并为未结束的轮次保留 `activeSince`。只有描述符和轮次边界会改变该值,因此 token 分片不会产生计时更新。
`registerContinuableSetup()` 允许可选包添加子级作用域功能,而无需让延续管理器知道这些功能的名称。贡献会在 Activation 发布前同步安装,在设置失败时一并回滚,并随子级作用域释放。新授权须等到下一个 Activation移除贡献则会立即撤销每个驻留安装项。
## 收集模型

View File

@@ -15,17 +15,25 @@
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./client": {
"types": "./lib/types/client.d.ts",
"default": "./lib/types/client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"dependencies": {
"zod": "^4.4.3"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-brand": "^0.0.1",
@@ -35,6 +43,7 @@
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
"@deepseek-ai/dsh-session-query": "^0.0.1",
"@deepseek-ai/dsh-session-projection": "^0.0.1",
"@deepseek-ai/dsh-tasks": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7"
@@ -46,6 +55,9 @@
"@deepseek-ai/dsh-session-query": {
"optional": true
},
"@deepseek-ai/dsh-session-projection": {
"optional": true
},
"@deepseek-ai/dsh-tasks": {
"optional": true
}
@@ -59,6 +71,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"

View File

@@ -0,0 +1,7 @@
/**
* Browser-safe subagent projection vocabulary.
*
* @module @deepseek-ai/dsh-subagent/client
*/
export type { SubagentTimingProjection } from './projection-types.ts'

View File

@@ -65,6 +65,7 @@ import type { ContinuableSetupContribution } from './activation-setup-registry.t
import { listChildren as listSubagentChildren } from './list-children.ts'
import type { SubagentListEntry } from './list-children.ts'
import { snapshotSubagentDescriptor } from './descriptor.ts'
import { subagentTimingProjectionDefinition } from './projection.ts'
export * from './out-of-process.ts'
export { SubagentRunId } from './types.ts'
@@ -117,6 +118,7 @@ export type {
export type { ContinuableSetupContribution } from './activation-setup-registry.ts'
export type { SubagentListEntry } from './list-children.ts'
export type { SubagentRunEndInfo, SubagentRunInfo } from './types.ts'
export type { SubagentTimingProjection } from './projection-types.ts'
declare module 'cordis' {
interface Context {
@@ -186,6 +188,9 @@ export class SubagentService extends Service {
if (this.continuations === manager) this.continuations = undefined
}, 'subagents.continuationBinding()')
})
ctx.inject(['sessionProjections'], (projectionCtx) => {
projectionCtx.sessionProjections.register(subagentTimingProjectionDefinition)
})
}
/**

View File

@@ -0,0 +1,20 @@
/**
* Pure client-safe subagent projection vocabulary.
*
* @module @deepseek-ai/dsh-subagent/projection-types
*/
/** Durable active-turn timing for one descriptor-backed child session. */
export interface SubagentTimingProjection {
/** Milliseconds accumulated across completed turns after the child's own descriptor. */
settledMs: number
/** Start of the currently open turn, when one has not reached `turn/end`. */
activeSince?: number
}
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
/** Active-turn duration for a descriptor-backed subagent session. */
subagentTiming: SubagentTimingProjection
}
}

View File

@@ -0,0 +1,68 @@
/**
* Pure session projection for subagent active-turn duration.
*
* @module @deepseek-ai/dsh-subagent/projection
*/
import { z } from 'zod'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import type { SubagentTimingProjection } from './projection-types.ts'
interface TimingState extends SubagentTimingProjection {
/** Latest pre-descriptor turn start, promoted when the child's own descriptor arrives. */
pendingTurnStart?: number
/** Whether the fold has crossed a descriptor in this logical log. */
descriptorSeen: boolean
}
const projectionSchema = z.object({
settledMs: z.number().int().nonnegative(),
activeSince: z.number().int().nonnegative().optional(),
}).strict() as unknown as z.ZodType<SubagentTimingProjection>
/**
* Fold turn boundaries around the child's own durable descriptor.
*
* A fork seed may contain an ancestor descriptor and completed turns. Every
* descriptor therefore resets the accumulated state; the healthy catalog
* admits only a child with exactly one descriptor in its own suffix, making
* the final reset the child's authoritative timing origin.
*/
export const subagentTimingProjectionDefinition:
ProjectionDefinition<'subagentTiming', TimingState> = {
key: 'subagentTiming',
schema: projectionSchema,
init: () => ({ descriptorSeen: false, settledMs: 0 }),
apply: (state, event) => {
if (event.type === 'turn/start') {
return state.descriptorSeen
? { ...state, activeSince: event.time }
: { ...state, pendingTurnStart: event.time }
}
if (event.type === 'subagent/descriptor') {
const activeSince = state.activeSince ?? state.pendingTurnStart
return {
descriptorSeen: true,
settledMs: 0,
...(activeSince === undefined ? {} : { activeSince }),
}
}
if (event.type !== 'turn/end') return state
if (!state.descriptorSeen) {
if (state.pendingTurnStart === undefined) return state
const { pendingTurnStart: _closed, ...next } = state
return next
}
if (state.activeSince === undefined) return state
const { activeSince, ...rest } = state
return {
...rest,
settledMs: state.settledMs + Math.max(0, event.time - activeSince),
}
},
view: state => ({
settledMs: state.settledMs,
...(state.activeSince === undefined ? {} : { activeSince: state.activeSince }),
}),
stateVersion: 1,
}

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import { subagentTimingProjectionDefinition } from '../src/projection.ts'
function event(type: SessionEvent['type'], seq: number, time: number): SessionEvent {
return { type, seq, time, data: {} } as SessionEvent
}
function fold(events: SessionEvent[]) {
let state = subagentTimingProjectionDefinition.init()
for (const item of events) state = subagentTimingProjectionDefinition.apply(state, item)
return subagentTimingProjectionDefinition.view(state)
}
describe('subagent timing projection', () => {
it('resets inherited seed timing at the child descriptor and sums later completed turns', () => {
expect(fold([
event('turn/start', 0, 100),
event('subagent/descriptor', 1, 110),
event('turn/end', 2, 300),
event('turn/start', 3, 1_000),
event('subagent/descriptor', 4, 1_100),
event('turn/end', 5, 4_100),
event('turn/start', 6, 10_000),
event('turn/end', 7, 12_000),
])).toEqual({ settledMs: 5_100 })
})
it('exposes an open turn start and never subtracts time for reversed boundaries', () => {
expect(fold([
event('turn/start', 0, 1_000),
event('subagent/descriptor', 1, 1_100),
event('turn/end', 2, 900),
event('turn/start', 3, 2_000),
event('assistant/chunk', 4, 2_500),
])).toEqual({ settledMs: 0, activeSince: 2_000 })
})
it('ignores completed pre-descriptor turns and unrelated events', () => {
const initial = subagentTimingProjectionDefinition.init()
expect(subagentTimingProjectionDefinition.apply(
initial,
event('assistant/chunk', 0, 1),
)).toBe(initial)
expect(fold([
event('turn/start', 0, 100),
event('turn/end', 1, 200),
event('subagent/descriptor', 2, 300),
])).toEqual({ settledMs: 0 })
})
})

View File

@@ -32,6 +32,9 @@
{
"path": "../../session-query/session-query"
},
{
"path": "../../session-projection/session-projection"
},
{
"path": "../../tasks/tasks"
},