Merge branch 'feat/ripgrep-packaged-binary' into feat/pwsh-tool
# Conflicts: # docs/module-graph.md
This commit is contained in:
@@ -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: e54f0b98ec3649cec428a47026e6657a9749608b
|
||||
README.zh.md: 1624fa59854d9b61770c5ef0f9d89f7882198da4
|
||||
|
||||
@@ -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 same-cut `active.since` and `active.through` bounds for an open turn. While that turn remains open, `active.through` follows the latest folded event, giving an inactive consumer a conservative crash bound without mixing in newer session metadata.
|
||||
|
||||
`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
|
||||
|
||||
@@ -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` 活跃时间,并为未结束的轮次保留同一切面的 `active.since` 和 `active.through` 边界。在该轮次保持未结束期间,`active.through` 会跟随最近折叠的事件,从而为 inactive 消费方提供保守的崩溃上界,又不会混入更新的会话元数据。
|
||||
|
||||
`registerContinuableSetup()` 允许可选包添加子级作用域功能,而无需让延续管理器知道这些功能的名称。贡献会在 Activation 发布前同步安装,在设置失败时一并回滚,并随子级作用域释放。新授权须等到下一个 Activation,移除贡献则会立即撤销每个驻留安装项。
|
||||
|
||||
## 收集模型
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { AgentSetupCommit } from '@deepseek-ai/dsh-agent'
|
||||
import { errorChain } from '@deepseek-ai/dsh-llm'
|
||||
import { SubagentError } from './error.ts'
|
||||
|
||||
@@ -47,17 +48,6 @@ interface TransactionState {
|
||||
invalidated: boolean
|
||||
}
|
||||
|
||||
/** Package-private setup transaction consumed by the continuation manager. */
|
||||
export interface ActivationSetupTransaction {
|
||||
/**
|
||||
* Reject a batch invalidated by revocation before publication.
|
||||
* @throws {SubagentError} code `ACTIVATION_SETUP_REVOKED` after revocation.
|
||||
*/
|
||||
assertIntact(): void
|
||||
/** Promote this batch to resident installations. */
|
||||
commit(): void
|
||||
}
|
||||
|
||||
/** Re-read mutable removal state after a contribution may have revoked itself. */
|
||||
function isRemoved(registration: Registration): boolean {
|
||||
return registration.removed
|
||||
@@ -95,9 +85,9 @@ export class SubagentActivationSetupRegistry {
|
||||
/**
|
||||
* Install every live contribution into one unpublished child context.
|
||||
* @param childCtx - the child's unpublished scoped context.
|
||||
* @returns the provisioning transaction.
|
||||
* @returns the provisioning commit consumed at Agent publication.
|
||||
*/
|
||||
apply(childCtx: Context): ActivationSetupTransaction {
|
||||
apply(childCtx: Context): AgentSetupCommit {
|
||||
const state: TransactionState = { installations: [], invalidated: false }
|
||||
try {
|
||||
for (const registration of [...this.registrations]) {
|
||||
@@ -135,15 +125,14 @@ export class SubagentActivationSetupRegistry {
|
||||
}
|
||||
childCtx.effect(() => () => { this.releaseChild(childCtx) }, 'subagents.activationSetup()')
|
||||
return {
|
||||
assertIntact: () => {
|
||||
if (!state.invalidated) return
|
||||
throw new SubagentError(
|
||||
'a continuable-subagent setup contribution was revoked while this child was being built; '
|
||||
+ 'the child was not established',
|
||||
'ACTIVATION_SETUP_REVOKED',
|
||||
)
|
||||
},
|
||||
commit: () => {
|
||||
if (state.invalidated) {
|
||||
throw new SubagentError(
|
||||
'a continuable-subagent setup contribution was revoked while this child was being built; '
|
||||
+ 'the child was not established',
|
||||
'ACTIVATION_SETUP_REVOKED',
|
||||
)
|
||||
}
|
||||
for (const installation of state.installations) installation.transaction = undefined
|
||||
},
|
||||
}
|
||||
|
||||
7
packages/subagent/subagent/src/client.ts
Normal file
7
packages/subagent/subagent/src/client.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Browser-safe subagent projection vocabulary.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent/client
|
||||
*/
|
||||
|
||||
export type { SubagentTimingProjection } from './projection-types.ts'
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
Agent,
|
||||
AgentHandle,
|
||||
AgentOptions,
|
||||
AgentSetupCommit,
|
||||
CreateAgentOptions,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
|
||||
@@ -42,7 +43,6 @@ import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentStartRequ
|
||||
import type { ActivationObserver } from './lifecycle.ts'
|
||||
import { SubagentError } from './error.ts'
|
||||
import type SubagentActivationSetupRegistry from './activation-setup-registry.ts'
|
||||
import type { ActivationSetupTransaction } from './activation-setup-registry.ts'
|
||||
|
||||
/** Attribution for a model coordinator's follow-up to one of its children. */
|
||||
export interface CoordinatorMessageSource {
|
||||
@@ -800,10 +800,9 @@ export class SubagentContinuationManager {
|
||||
// `AgentRegistry.enter()` is the authoritative collision boundary for an id
|
||||
// some other owner holds — a duplicate would reject there with rollback.
|
||||
inputs.signal.throwIfAborted()
|
||||
let setupTransaction!: ActivationSetupTransaction
|
||||
const setup = (childCtx: Context): void => {
|
||||
const setup = (childCtx: Context): AgentSetupCommit => {
|
||||
applyChildComposition(childCtx, inputs.composition)
|
||||
setupTransaction = this.setupRegistry.apply(childCtx)
|
||||
return this.setupRegistry.apply(childCtx)
|
||||
}
|
||||
const observer = this.host.observeActivation(provider, childId, parent)
|
||||
const { create } = inputs
|
||||
@@ -842,7 +841,6 @@ export class SubagentContinuationManager {
|
||||
try {
|
||||
inputs.signal.throwIfAborted()
|
||||
this.assertAdmitting(parent)
|
||||
setupTransaction.assertIntact()
|
||||
this.acquireOwnership(parent, childId)
|
||||
// Every accepted id leaves the inbox exactly once, through dequeue or
|
||||
// discard. Clearing it there is what lets `stateOf()` distinguish a truly
|
||||
@@ -860,8 +858,8 @@ export class SubagentContinuationManager {
|
||||
for (const item of items) activation.accepted.delete(item.message.id)
|
||||
this.wake(activation)
|
||||
})
|
||||
// Resident setup revokes live from here instead of invalidating creation.
|
||||
setupTransaction.commit()
|
||||
// Agent creation committed setup at its publication boundary;
|
||||
// revocations from here on are immediate live revocation.
|
||||
// Publish the start edge before any turn can run, so observers see this
|
||||
// epoch before its first request.
|
||||
observer.start(handle.agent)
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
* omits `subagentDepth` — cold resume trusts the persisted header's
|
||||
* `delegationDepth` as the monotone floor — and `outputSchema`, which belongs
|
||||
* to one activation's result contract rather than durable child composition.
|
||||
* Per-activation knobs such as `maxTokens` are omitted for the same reason as
|
||||
* `outputSchema`: they budget one activation. Cold resume requires the exact
|
||||
* live parent for authorization but reconstructs child options only from the
|
||||
* durable descriptor, so it neither restores the prior budget nor inherits
|
||||
* the parent's current one; the resumed route's defaults apply instead.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent/descriptor
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
25
packages/subagent/subagent/src/projection-types.ts
Normal file
25
packages/subagent/subagent/src/projection-types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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
|
||||
/** Same-cut bounds of the currently open turn, when one has not reached `turn/end`. */
|
||||
active?: {
|
||||
/** Start of the open turn. */
|
||||
since: number
|
||||
/** Latest event time folded into this projection cut. */
|
||||
through: number
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||
interface SessionProjectionMap {
|
||||
/** Active-turn duration for a descriptor-backed subagent session. */
|
||||
subagentTiming: SubagentTimingProjection
|
||||
}
|
||||
}
|
||||
82
packages/subagent/subagent/src/projection.ts
Normal file
82
packages/subagent/subagent/src/projection.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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 {
|
||||
/** Milliseconds accumulated across completed post-descriptor turns. */
|
||||
settledMs: number
|
||||
/** Current open interval kept paired inside the fold. */
|
||||
active?: { since: number; through: number }
|
||||
/** 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
|
||||
}
|
||||
|
||||
// Zod's optional output includes explicit `undefined`; with
|
||||
// exactOptionalPropertyTypes the public interface permits omission only.
|
||||
const projectionSchema = z.object({
|
||||
settledMs: z.number().int().nonnegative(),
|
||||
active: z.object({
|
||||
since: z.number().int().nonnegative(),
|
||||
through: z.number().int().nonnegative(),
|
||||
}).strict().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, active: { since: event.time, through: event.time } }
|
||||
: { ...state, pendingTurnStart: event.time }
|
||||
}
|
||||
if (event.type === 'subagent/descriptor') {
|
||||
const activeSince = state.active?.since ?? state.pendingTurnStart
|
||||
return {
|
||||
descriptorSeen: true,
|
||||
settledMs: 0,
|
||||
...(activeSince === undefined
|
||||
? {}
|
||||
: { active: { since: activeSince, through: event.time } }),
|
||||
}
|
||||
}
|
||||
if (event.type === 'turn/end') {
|
||||
if (!state.descriptorSeen) {
|
||||
if (state.pendingTurnStart === undefined) return state
|
||||
const { pendingTurnStart: _closed, ...next } = state
|
||||
return next
|
||||
}
|
||||
if (state.active === undefined) return state
|
||||
const { active, ...rest } = state
|
||||
return {
|
||||
...rest,
|
||||
settledMs: state.settledMs + Math.max(0, event.time - active.since),
|
||||
}
|
||||
}
|
||||
if (state.active === undefined) return state
|
||||
return { ...state, active: { ...state.active, through: event.time } }
|
||||
},
|
||||
view: state => ({
|
||||
settledMs: state.settledMs,
|
||||
...(state.active === undefined ? {} : { active: state.active }),
|
||||
}),
|
||||
stateVersion: 2,
|
||||
}
|
||||
@@ -19,8 +19,7 @@ describe('SubagentActivationSetupRegistry', () => {
|
||||
|
||||
const transaction = registry.apply(child.ctx)
|
||||
expect(order).toEqual(['first', 'second'])
|
||||
expect(() => { transaction.assertIntact() }).not.toThrow()
|
||||
transaction.commit()
|
||||
expect(() => { transaction.commit() }).not.toThrow()
|
||||
expect(order).toEqual(['first', 'second'])
|
||||
})
|
||||
|
||||
@@ -68,7 +67,7 @@ describe('SubagentActivationSetupRegistry', () => {
|
||||
|
||||
remove()
|
||||
expect(disposals).toBe(1)
|
||||
expect(() => { transaction.assertIntact() }).toThrow(/revoked while this child was being built/)
|
||||
expect(() => { transaction.commit() }).toThrow(/revoked while this child was being built/)
|
||||
})
|
||||
|
||||
it('catches a contribution revoked inside its own installer', () => {
|
||||
@@ -82,7 +81,7 @@ describe('SubagentActivationSetupRegistry', () => {
|
||||
|
||||
const transaction = registry.apply(childContext().ctx)
|
||||
expect(disposals).toBe(1)
|
||||
expect(() => { transaction.assertIntact() }).toThrow(/revoked/)
|
||||
expect(() => { transaction.commit() }).toThrow(/revoked/)
|
||||
})
|
||||
|
||||
it('attempts every contribution-removal disposer before reporting failures', () => {
|
||||
|
||||
80
packages/subagent/subagent/tests/timing-projection.spec.ts
Normal file
80
packages/subagent/subagent/tests/timing-projection.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import SubagentService from '../src/index.ts'
|
||||
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('registers with the optional session projection registry', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
const serviceFiber = await ctx.plugin(SubagentService)
|
||||
|
||||
expect(ctx.sessionProjections.snapshot(ctx.sessions.create()).values.subagentTiming)
|
||||
.toEqual({ settledMs: 0 })
|
||||
await serviceFiber.dispose()
|
||||
expect(ctx.sessionProjections.snapshot(ctx.sessions.create()).values.subagentTiming)
|
||||
.toBeUndefined()
|
||||
})
|
||||
|
||||
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, active: { since: 2_000, through: 2_500 } })
|
||||
})
|
||||
|
||||
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(subagentTimingProjectionDefinition.apply(
|
||||
initial,
|
||||
event('turn/end', 1, 2),
|
||||
)).toBe(initial)
|
||||
const descriptor = subagentTimingProjectionDefinition.apply(
|
||||
initial,
|
||||
event('subagent/descriptor', 2, 3),
|
||||
)
|
||||
expect(subagentTimingProjectionDefinition.apply(
|
||||
descriptor,
|
||||
event('turn/end', 3, 4),
|
||||
)).toBe(descriptor)
|
||||
expect(fold([
|
||||
event('turn/start', 0, 100),
|
||||
event('turn/end', 1, 200),
|
||||
event('subagent/descriptor', 2, 300),
|
||||
])).toEqual({ settledMs: 0 })
|
||||
})
|
||||
})
|
||||
@@ -32,6 +32,9 @@
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
{
|
||||
"path": "../../session-projection/session-projection"
|
||||
},
|
||||
{
|
||||
"path": "../../tasks/tasks"
|
||||
},
|
||||
|
||||
@@ -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/tool-subagent-report/README.md
|
||||
README.md: e15b8b5d5881fd7b6868995fec22048a605f4c7e
|
||||
README.zh.md: 0c41bc9c1e5aa4d728789b064f2d00c8da8ca6c8
|
||||
README.md: cd73154dfb9c8b37f4a811c3beedbe6a63207f58
|
||||
README.zh.md: 4b31bed48ea0e50ec3a9d507548658defb94b8b8
|
||||
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
The optional child-scoped `report` tool is a thin adapter over `ctx.subagents.reportFrom()`. It gives every continuable in-process child a return channel to the Agent that started it. The package registers a continuable-child setup contribution instead of a global tool, so `report` exists only inside those children. Roots, one-shot subagents, remote subagent providers, sibling scopes, and agentless tool execution never present or execute it. Installing this package grants only that child-scoped capability; the parent-to-child direction remains the independent [`@deepseek-ai/dsh-tool-subagent-control`](../tool-subagent-control/README.md), and continuable mode depends on neither package.
|
||||
|
||||
A child may call `report` zero or many times in one turn. A successful call neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically. The tool accepts no recipient: `exec.agent` is the sender's exact live Agent and the authority credential, and the service derives the sole recipient from that child's durable `parentSession`. Success returns the stable `MessageId` of the parent-accepted message, not a read receipt, an inbox-occurrence id, a parent-log acknowledgement, a turn-completion receipt, or a persistence flush. A missing, disposed, or closing parent fails the call with `direct parent is not live; report was not delivered`; the service performs no injection, parent cold resume, or offline mailbox write, so the durable child transcript remains the recovery source.
|
||||
A child may call `report` zero or many times in one turn. A successful call neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically. The tool accepts no recipient: `exec.agent` is the sender's exact live Agent and the authority credential, and the service derives the sole recipient from that child's durable `parentSession`. Success returns the stable `MessageId` of the parent-accepted message, not a read receipt, an inbox-occurrence id, a parent-log acknowledgement, a turn-completion receipt, or a persistence flush. A parent absent from the registry fails the call with `direct parent is not live; report was not delivered` — registry presence governs parent resolution, and a registered parent already in host-owned disposal still accepts while its log admits appends. The service performs no injection, parent cold resume, or offline mailbox write; the durable child transcript remains the recovery source, and a failed tool call does not prove non-delivery (a later `tools/post-execute` veto can fail a call whose report was already accepted).
|
||||
|
||||
`reportDelivery` selects parent scheduling for every accepted report. `quiet` (the default) uses `parent.inject()`, adding model-facing context without starting a parent model request: an idle parent's append completes before the call returns, while a report reaching an admitting or running parent stages for the next safe log position. `wakeup` uses `parent.followup()`, creating exactly one ordinary later parent turn and waking a parked parent driver; it never steers an open turn. This is deployment scheduling policy, so the model-facing schema cannot select or override it per call.
|
||||
|
||||
@@ -58,7 +58,6 @@ Append-only; the report follows the parent's reusable request prefix. Waking del
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Setup revocation can follow lower-level Session publication** — the final revocation check runs after `ctx.agents.create()` or `ctx.agents.resume()` returns, by which point that call has already published its Agent and Session. Revocation in this window rolls back the handle and prevents the subagent Activation start edge, but may leave a persisted Session. Closing this gap requires a future Agent-creation setup transaction seam before lower-level publication.
|
||||
- **A parent whose host-owned disposal already started can still accept** — `AgentHandle.dispose()` cancels, awaits quiescence, and only then unwinds the scope and leaves the registry; it exposes no signal for "disposal started." A report accepted in that window is appended to the parent's transcript, but that parent will not act on it in this process. A continuation-manager-owned parent rejects forest teardown through the manager's admission boundary.
|
||||
- **Acceptance is weaker than durable delivery** — there is no durable mailbox, idempotency key, delivery receipt, retry protocol, or exactly-once claim. A process failure after one side recorded acceptance leaves the outcome ambiguous, and an external retry may duplicate the report.
|
||||
- **A staged quiet report is not immediately reconstructable** — acceptance returns its stable `MessageId`, but the parent Session reconstructs the framed content only after pending context reaches its ordinary log boundary.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
可选的子级作用域 `report` 工具是 `ctx.subagents.reportFrom()` 之上的轻量适配器。它为每个可继续的进程内子级提供一条返回通道,指向启动该子级的 Agent(智能体)。本包(package)注册的是可继续子级设置贡献,而不是全局工具,因此 `report` 只存在于这些子级内部。根 Agent、一次性 subagent、远程 subagent 提供方、同级作用域以及不关联 Agent 的工具执行都不会提供或执行它。安装本包只授予这项子级作用域功能;父到子方向仍由独立的 [`@deepseek-ai/dsh-tool-subagent-control`](../tool-subagent-control/README.md) 负责,可继续模式不依赖这两个包中的任一个。
|
||||
|
||||
子级可以在一个轮次中调用 `report` 零次或多次。调用成功既不会结束轮次或结算 Activation,也不会阻止父级后续消息;轮次结束也绝不会自动上报。该工具不接受接收方参数:`exec.agent` 是发送方准确的实时 Agent,也是权限凭据;服务根据该子级持久化的 `parentSession` 推导唯一接收方。成功时返回父级已接受消息的稳定 `MessageId`,不表示已读回执、inbox 中该次出现的 id、父级日志确认、轮次完成回执或持久化刷盘。父级不存在、已 dispose(资源释放)或正在关闭时,本次调用会失败并返回 `direct parent is not live; report was not delivered`;服务不会执行注入、父级冷恢复或离线 mailbox 写入,因此持久化子级 transcript(文本记录)仍是恢复真源。
|
||||
子级可以在一个轮次中调用 `report` 零次或多次。调用成功既不会结束轮次或结算 Activation,也不会阻止父级后续消息;轮次结束也绝不会自动上报。该工具不接受接收方参数:`exec.agent` 是发送方准确的实时 Agent,也是权限凭据;服务根据该子级持久化的 `parentSession` 推导唯一接收方。成功时返回父级已接受消息的稳定 `MessageId`,不表示已读回执、inbox 中该次出现的 id、父级日志确认、轮次完成回执或持久化刷盘。父级解析由注册表中的存在性决定:父级不在注册表时,调用失败并返回 `direct parent is not live; report was not delivered`;已开始宿主 dispose 但仍在注册表中的父级在其日志仍接受追加时仍会接受。服务不会执行注入、父级冷恢复或离线 mailbox 写入;持久化子级 transcript(文本记录)仍是恢复真源,且工具调用失败不能证明未送达(后续 `tools/post-execute` 否决可能让报告已被接受的调用以失败结束)。
|
||||
|
||||
`reportDelivery` 为每条已接受的报告选择父级调度方式。`quiet`(默认值)使用 `parent.inject()`,在不启动父级模型请求的情况下添加面向模型的上下文:父级空闲时,追加操作会在调用返回前完成;报告到达正在准入或运行的父级时,则会暂存到下一个安全日志位置。`wakeup` 使用 `parent.followup()`,准确创建一个普通的后续父级轮次,并唤醒停驻的父级驱动;它绝不会对正在运行的轮次进行 steering(中途引导)。这是部署调度策略,因此面向模型的 schema 不能在单次调用中选择或覆盖该策略。
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **setup 撤销可能发生在底层 Session 发布之后**:最终撤销检查发生在 `ctx.agents.create()` 或 `ctx.agents.resume()` 返回之后,此时该调用已发布其 Agent 和 Session。在这个窗口内撤销会回滚 handle,并阻止 subagent Activation 的 start 边,但可能留下持久化 Session。要弥合这个缺口,需要未来在底层发布之前提供 Agent 创建 setup 事务 seam。
|
||||
- **父级可能在宿主启动 dispose 后继续接受报告**:`AgentHandle.dispose()` 会先取消并等待完全停稳,然后才撤销作用域并离开注册表;它不公开「dispose 已开始」信号。在该窗口内接受的报告会追加到父级 transcript,但该父级不会在本进程中处理它。对于由延续管理器拥有的父级,管理器的准入边界会在整棵子树拆卸期间拒绝该上报。
|
||||
- **接受弱于持久投递**:没有持久化 mailbox、幂等键、投递回执、重试协议,也不保证恰好一次。任一侧记录接受后若进程失败,结果都不明确;外部重试可能产生重复上报。
|
||||
- **暂存的静默报告无法立即重建**:接受时会返回其稳定 `MessageId`,但只有当待处理上下文到达普通日志边界后,父级 Session 才能重建带前缀的内容。
|
||||
|
||||
@@ -88,7 +88,10 @@ export function installReportTool(
|
||||
* @param config - deployment scheduling policy.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config = {}): void {
|
||||
const { reportDelivery = 'quiet' } = Config(config)
|
||||
// Config() applies the schema default ('quiet') at runtime; the schemastery
|
||||
// return type keeps the input's optional shape, so assert the resolved
|
||||
// shape here — no runtime fallback exists or is wanted.
|
||||
const { reportDelivery } = Config(config) as { reportDelivery: SubagentReportDelivery }
|
||||
ctx.subagents.registerContinuableSetup(childCtx =>
|
||||
installReportTool(childCtx, ctx, reportDelivery))
|
||||
}
|
||||
|
||||
@@ -327,6 +327,15 @@ describe('dsh-tool-subagent-report', () => {
|
||||
return dispose
|
||||
})
|
||||
|
||||
// No session may be announced for the rejected child: the setup
|
||||
// validation must reject inside the creation callback, before the factory
|
||||
// publishes — a post-publication rejection would persist a resumable
|
||||
// ghost that `list_agents` surfaces and `send_message` can resurrect.
|
||||
// The parent was created inside setup(), so any later announcement is the
|
||||
// rejected child's.
|
||||
const announced: SessionId[] = []
|
||||
const listener = (session: { id: SessionId }): void => { announced.push(session.id) }
|
||||
const removeListener = ctx.on('session/created', listener)
|
||||
await expect(ctx.subagents.startContinuable({
|
||||
provider: 'spawn',
|
||||
label: 'racing child',
|
||||
@@ -336,9 +345,56 @@ describe('dsh-tool-subagent-report', () => {
|
||||
},
|
||||
signal: testSignal,
|
||||
})).rejects.toMatchObject({ code: 'ACTIVATION_SETUP_REVOKED' })
|
||||
removeListener()
|
||||
expect(announced).toEqual([])
|
||||
expect(ctx.agents.list().map(agent => agent.id)).toEqual([parent.id])
|
||||
})
|
||||
|
||||
it('rolls back materialization when setup revocation lands before publication', async () => {
|
||||
const { ctx, parent } = await setup({ load: false })
|
||||
const self: { revoke?: () => void } = {}
|
||||
let installed = false
|
||||
self.revoke = ctx.subagents.registerContinuableSetup(() => {
|
||||
installed = true
|
||||
queueMicrotask(() => { self.revoke?.() })
|
||||
return () => { installed = false }
|
||||
})
|
||||
const announced: SessionId[] = []
|
||||
const removeListener = ctx.on('session/created', (session) => { announced.push(session.id) })
|
||||
|
||||
await expect(ctx.subagents.startContinuable({
|
||||
provider: 'spawn',
|
||||
label: 'revoked child',
|
||||
request: {
|
||||
prompt: [{ type: 'text', text: 'revoked child' }],
|
||||
parent,
|
||||
},
|
||||
signal: testSignal,
|
||||
})).rejects.toMatchObject({ code: 'ACTIVATION_SETUP_REVOKED' })
|
||||
removeListener()
|
||||
expect(installed).toBe(false)
|
||||
expect(announced).toEqual([])
|
||||
expect(ctx.agents.list().map(agent => agent.id)).toEqual([parent.id])
|
||||
expect(ctx.sessions.list()).toEqual([parent.session])
|
||||
})
|
||||
|
||||
it('accepts a report into a host-disposing but still-registered parent', async () => {
|
||||
const { ctx } = await setup()
|
||||
const parentHandle = await ctx.agents.create({
|
||||
sessionId: SessionId('disposing-parent'),
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
const { child } = await startChild(ctx, parentHandle.agent)
|
||||
// Host-owned disposal starts asynchronously; the parent stays registered
|
||||
// until quiescence, and registry presence — not disposal state — is the
|
||||
// acceptance gate (pins the README contract).
|
||||
const disposing = parentHandle.dispose()
|
||||
const accepted = await callReport(ctx, child, 'during-close')
|
||||
expect(accepted.isError).toBe(false)
|
||||
await disposing
|
||||
expect((await callReport(ctx, child, 'after-close')).isError).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the namespace plugin shape and validates its default', () => {
|
||||
expect('default' in tool).toBe(false)
|
||||
expect(tool.name).toBe('tool-subagent-report')
|
||||
|
||||
Reference in New Issue
Block a user