refactor: hide remaining subagent helpers
This commit is contained in:
@@ -6,7 +6,7 @@ The fork provider creates an in-process child seeded with the parent's completed
|
||||
|
||||
The parent's current tool-calling turn is still open when a subagent starts: its log contains the assistant tool call but not the matching tool result or `turn/end`. Copying that raw log would give the child an invalid, unbalanced session.
|
||||
|
||||
Fork therefore uses `completedTurnPrefix(parent.session.events)`: the contiguous prefix ending at the last `turn/end`. The child sees all completed parent turns and none of the in-flight turn. If the parent has not completed a turn yet, the seed is empty and the child behaves like a fresh spawn.
|
||||
Fork therefore computes the contiguous prefix ending at the last `turn/end`. The child sees all completed parent turns and none of the in-flight turn. If the parent has not completed a turn yet, the seed is empty and the child behaves like a fresh spawn.
|
||||
|
||||
The seed transfers conversation history only. The child still receives a fresh flat registration scope; it does not inherit the parent's tool restrictions or authority.
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export const Config: z<Config> = z.object({
|
||||
* @param parent - the agent whose session log to slice.
|
||||
* @returns the seed events, contiguous from seq 0; empty when no turn has completed.
|
||||
*/
|
||||
export function completedTurnPrefix(parent: Agent): SessionEvent[] {
|
||||
function completedTurnPrefix(parent: Agent): SessionEvent[] {
|
||||
const events = parent.session.events
|
||||
const lastEnd = events.findLast(e => e.type === 'turn/end')
|
||||
if (lastEnd === undefined) return []
|
||||
|
||||
@@ -14,7 +14,6 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent
|
||||
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import * as fork from '../src/index.ts'
|
||||
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-inprocess'
|
||||
import { completedTurnPrefix } from '../src/index.ts'
|
||||
|
||||
type Script = ConstructorParameters<typeof MockAdapter>[0]
|
||||
|
||||
@@ -52,28 +51,6 @@ function text(blocks: { type: string; text?: string }[]): string {
|
||||
return blocks.filter(b => b.type === 'text').map(b => b.text).join('')
|
||||
}
|
||||
|
||||
describe('completedTurnPrefix', () => {
|
||||
it('returns an empty prefix for a parent that has never completed a turn', async () => {
|
||||
const { parent } = await setup([])
|
||||
expect(completedTurnPrefix(parent)).toEqual([])
|
||||
})
|
||||
|
||||
it('returns the balanced prefix up to and including the last turn/end', async () => {
|
||||
const { parent } = await setup([textResponse('first'), textResponse('second')])
|
||||
parent.send([{ type: 'text', text: 'q1' }])
|
||||
await parent.whenIdle()
|
||||
parent.send([{ type: 'text', text: 'q2' }])
|
||||
await parent.whenIdle()
|
||||
|
||||
const prefix = completedTurnPrefix(parent)
|
||||
// Ends exactly at the last turn/end; seq is contiguous from 0.
|
||||
expect(prefix.at(-1)?.type).toBe('turn/end')
|
||||
expect(prefix.map(e => e.seq)).toEqual(prefix.map((_, i) => i))
|
||||
// Both completed turns are present.
|
||||
expect(prefix.filter(e => e.type === 'turn/end')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dsh-subagent-fork', () => {
|
||||
it('emits subagent/start only after the seeded child is published', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('child answer')])
|
||||
@@ -96,7 +73,6 @@ describe('dsh-subagent-fork', () => {
|
||||
// The parent has never completed a turn → empty prefix → the provider omits
|
||||
// the seed → the child runs fresh. Exercises the `seed.length > 0` false arm.
|
||||
const { ctx, parent } = await setup([textResponse('fresh child')])
|
||||
expect(completedTurnPrefix(parent)).toEqual([])
|
||||
const run = await start(ctx, 'fork', { prompt: [{ type: 'text', text: 'child q' }], parent })
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe('completed')
|
||||
@@ -104,6 +80,24 @@ describe('dsh-subagent-fork', () => {
|
||||
const child = ctx.agents.get(run.id)!
|
||||
// Only the child's own turn — no seeded parent turns.
|
||||
expect(child.session.events.filter(e => e.type === 'turn/end')).toHaveLength(1)
|
||||
expect(child.session.header.seedLength).toBeUndefined()
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('seeds every completed parent turn through the last turn/end', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('first'), textResponse('second'), textResponse('child')])
|
||||
parent.send([{ type: 'text', text: 'q1' }])
|
||||
await parent.whenIdle()
|
||||
parent.send([{ type: 'text', text: 'q2' }])
|
||||
await parent.whenIdle()
|
||||
const parentPrefixLen = parent.session.events.length
|
||||
|
||||
const run = await start(ctx, 'fork', { prompt: [{ type: 'text', text: 'child q' }], parent })
|
||||
await run.result
|
||||
const child = ctx.agents.get(run.id)!
|
||||
expect(child.session.header.seedLength).toBe(parentPrefixLen)
|
||||
expect(child.session.events.slice(0, parentPrefixLen).at(-1)?.type).toBe('turn/end')
|
||||
expect(child.session.events.slice(0, parentPrefixLen).filter(e => e.type === 'turn/end')).toHaveLength(2)
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -161,13 +161,13 @@ function stopReasonError(result: SubagentResult): string | undefined {
|
||||
* A fresh child needs a standalone prompt; a forked child already sees the
|
||||
* conversation's completed turns — telling the model to restate everything
|
||||
* (or, worse, that the child "does not see this conversation") would be false
|
||||
* for a fork. Exported for tests.
|
||||
* for a fork.
|
||||
* @param inheritsConversation - whether the child's conversation is seeded
|
||||
* with the parent's completed turns; this says nothing about tool, service,
|
||||
* scope, or authority inheritance.
|
||||
* @returns the tool `description` and the `prompt` parameter description.
|
||||
*/
|
||||
export function providerWording(inheritsConversation: boolean): { description: string; promptDescription: string } {
|
||||
function providerWording(inheritsConversation: boolean): { description: string; promptDescription: string } {
|
||||
if (inheritsConversation) {
|
||||
return {
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user