Merge branch 'codex/simp-ui-identity-residue' into codex/simp-hide-concrete-agent-loop
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md
This commit is contained in:
@@ -131,7 +131,7 @@ export interface Config {
|
|||||||
|
|
||||||
Depends on: [`AgentOptions`](../packages/core/agent/src/index.ts) · [`SessionId`](../packages/core/session/src/index.ts)
|
Depends on: [`AgentOptions`](../packages/core/agent/src/index.ts) · [`SessionId`](../packages/core/session/src/index.ts)
|
||||||
|
|
||||||
Source: [`packages/core/agent-loop/src/index.ts:323`](../packages/core/agent-loop/src/index.ts)
|
Source: [`packages/core/agent-loop/src/index.ts:332`](../packages/core/agent-loop/src/index.ts)
|
||||||
|
|
||||||
## `@deepseek-ai/dsh-bash-local`
|
## `@deepseek-ai/dsh-bash-local`
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandl
|
|||||||
|
|
||||||
Types: [Agent](../core-data-structures/core.md)
|
Types: [Agent](../core-data-structures/core.md)
|
||||||
|
|
||||||
Source: [`packages/core/agent-loop/src/index.ts:338`](../../packages/core/agent-loop/src/index.ts)
|
Source: [`packages/core/agent-loop/src/index.ts:347`](../../packages/core/agent-loop/src/index.ts)
|
||||||
|
|
||||||
## `ctx.agents` — `AgentRegistry`
|
## `ctx.agents` — `AgentRegistry`
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ interface SubagentRun {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A local run MUST publish an ordinary child agent/session before `start()` fulfills, return that child session id as `SubagentRun.id`, and record `request.parent.session.id` in the child's `parentSession` header. Runtime ownership may place the child under the parent, provider, or root scope; `parentSession` is the durable transport-neutral lineage. A remote provider instead returns a parent-scoped lifecycle id and does not publish a local child.
|
||||||
|
|
||||||
## The provider seam: `SubagentProvider`
|
## The provider seam: `SubagentProvider`
|
||||||
|
|
||||||
One transport for running a child agent. Implementations register under a unique name via `SubagentService.registerProvider`; multiple coexist in one context. The service validates every requested start-time capability before calling `start`, so an implementation may assume e.g. `request.maxDepth` is honorable when present. `inheritsParentContext` is a DESCRIPTIVE fact beside the capabilities (nothing validates against it): whether a child sees the parent conversation (`fork`: true, `spawn`/`acp`: false) — the model-facing consumer derives truthful tool wording from it. It describes conversation history only, not tool registrations, injected services, or authority inheritance.
|
One transport for running a child agent. Implementations register under a unique name via `SubagentService.registerProvider`; multiple coexist in one context. The service validates every requested start-time capability before calling `start`, so an implementation may assume e.g. `request.maxDepth` is honorable when present. `inheritsParentContext` is a DESCRIPTIVE fact beside the capabilities (nothing validates against it): whether a child sees the parent conversation (`fork`: true, `spawn`/`acp`: false) — the model-facing consumer derives truthful tool wording from it. It describes conversation history only, not tool registrations, injected services, or authority inheritance.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const INACTIVE_STATES: ReadonlySet<FiberState> = new Set([
|
|||||||
class FactoryOwnership {
|
class FactoryOwnership {
|
||||||
private accepting = true
|
private accepting = true
|
||||||
private transactions = new Set<AgentCreationTransaction>()
|
private transactions = new Set<AgentCreationTransaction>()
|
||||||
|
private startupTasks = new Set<Promise<void>>()
|
||||||
|
|
||||||
constructor(private readonly fiber: Context['fiber']) {}
|
constructor(private readonly fiber: Context['fiber']) {}
|
||||||
|
|
||||||
@@ -56,12 +57,20 @@ class FactoryOwnership {
|
|||||||
return () => { this.transactions.delete(transaction) }
|
return () => { this.transactions.delete(transaction) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Join config startup work that begins before an agent transaction exists. */
|
||||||
|
trackStartup(task: Promise<void>): void {
|
||||||
|
this.startupTasks.add(task)
|
||||||
|
const forget = () => { this.startupTasks.delete(task) }
|
||||||
|
void task.then(forget, forget)
|
||||||
|
}
|
||||||
|
|
||||||
async dispose(): Promise<void> {
|
async dispose(): Promise<void> {
|
||||||
this.accepting = false
|
this.accepting = false
|
||||||
const reason = new Error('agent loop is not active')
|
const reason = new Error('agent loop is not active')
|
||||||
await Promise.all(
|
await Promise.all([
|
||||||
[...this.transactions].map(transaction => transaction.disposeForFactory(reason)),
|
...[...this.transactions].map(transaction => transaction.disposeForFactory(reason)),
|
||||||
)
|
...this.startupTasks,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +379,10 @@ export class AgentLoop extends Service implements AgentFactory {
|
|||||||
if (persistence === undefined) {
|
if (persistence === undefined) {
|
||||||
this.create(configuredId, options, meta)
|
this.create(configuredId, options, meta)
|
||||||
} else {
|
} else {
|
||||||
void this.restoreOrCreateConfigured(ctx, persistence, configuredId, options, meta).catch((error: unknown) => {
|
const startup = this.restoreOrCreateConfigured(ctx, persistence, configuredId, options, meta).catch((error: unknown) => {
|
||||||
ctx.logger.warn(`agent "${id}": config-driven restore of "${configuredId}" failed: ${String(error)}`)
|
ctx.logger.warn(`agent "${id}": config-driven restore of "${configuredId}" failed: ${String(error)}`)
|
||||||
})
|
})
|
||||||
|
this.ownership.trackStartup(startup)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -402,6 +412,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
|||||||
meta: Pick<SessionHeader, 'cwd'>,
|
meta: Pick<SessionHeader, 'cwd'>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const exists = (await persistence.list()).some(header => header.id === sessionId)
|
const exists = (await persistence.list()).some(header => header.id === sessionId)
|
||||||
|
if (!this.ownership.isActive()) return
|
||||||
if (exists) {
|
if (exists) {
|
||||||
await this.resumeWith(ownerCtx, persistence, { resumeSessionId: sessionId, agentOptions })
|
await this.resumeWith(ownerCtx, persistence, { resumeSessionId: sessionId, agentOptions })
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -113,6 +113,31 @@ describe('config-driven session id', () => {
|
|||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('joins an exact-id persistence lookup before AgentLoop disposal completes', async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-dispose-'))
|
||||||
|
dirs.push(root)
|
||||||
|
const ctx = await makeCoreContext()
|
||||||
|
await ctx.plugin(SessionPersistenceJsonl, { root })
|
||||||
|
const listing = Promise.withResolvers<Awaited<ReturnType<typeof ctx.sessionPersistence.list>>>()
|
||||||
|
vi.spyOn(ctx.sessionPersistence, 'list').mockReturnValue(listing.promise)
|
||||||
|
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
|
||||||
|
|
||||||
|
const loop = await ctx.plugin(AgentLoop, {
|
||||||
|
agents: [{ id: 'main', sessionId: SessionId('stdio-exact-dispose'), model: 'mock' }],
|
||||||
|
})
|
||||||
|
let disposed = false
|
||||||
|
const disposal = loop.dispose().then(() => { disposed = true })
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(disposed).toBe(false)
|
||||||
|
|
||||||
|
listing.resolve([])
|
||||||
|
await disposal
|
||||||
|
expect(ctx.agents.get(SessionId('stdio-exact-dispose'))).toBeUndefined()
|
||||||
|
expect(warn).not.toHaveBeenCalled()
|
||||||
|
warn.mockRestore()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
it('identity-nests the deferred resume fiber under its labeled owner effect', async () => {
|
it('identity-nests the deferred resume fiber under its labeled owner effect', async () => {
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
await ctx.plugin(LlmService)
|
await ctx.plugin(LlmService)
|
||||||
|
|||||||
Reference in New Issue
Block a user