Merge branch 'codex/simp-agent-entry-state' into codex/simp-unify-agent-session-id
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md # docs/rfc/implemented/architecture/2026-06-20-branded-ids.md # docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md # docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md # docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.md # packages/bash/tool-bash/README.md # packages/bash/tool-bash/src/index.ts # packages/bash/tool-bash/tests/tools.spec.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/tests/properties.spec.ts # packages/core/agent/README.md # packages/core/agent/src/index.ts # packages/guard/repeat-tool-guard/src/index.ts # packages/hooks/hooks-claude/tests/bridge.spec.ts # packages/subagent/subagent-acp/tests/mock-acp-server.ts # packages/ui/acp/tests/dispose.spec.ts # packages/ui/stdio-agent/src/index.ts # packages/ui/stdio-agent/src/stdio-chat.ts # packages/ui/stdio-agent/tests/stdio-chat.spec.ts # packages/util/brand/src/index.ts
This commit is contained in:
@@ -1,22 +1,9 @@
|
||||
/**
|
||||
* The in-process FORK subagent backend: registers a {@link SubagentProvider} on
|
||||
* `ctx.subagents` that runs each child as a child {@link Agent} SEEDED with a
|
||||
* prefix of the parent's session log — so the child inherits the parent's
|
||||
* conversation context instead of starting fresh. The run mechanics live in
|
||||
* `@deepseek-ai/dsh-subagent-inprocess` ({@link startInProcessRun}); this
|
||||
* backend just computes the seed. The spawn backend is an independent peer over
|
||||
* the same driver.
|
||||
*
|
||||
* The seed boundary is the crux: at the moment a subagent tool's `execute`
|
||||
* runs, the parent's CURRENT turn is open and unbalanced (it holds the
|
||||
* `assistant/message` with this spawn's tool-call, plus the dangling `tool/call`
|
||||
* with no `tool/result`). Seeding that raw prefix gives the child an open turn
|
||||
* the session constructor and the dev-mode invariants replay REJECT. So the
|
||||
* fork seeds only the **balanced completed-turn prefix**: the parent's log up
|
||||
* to and including its last `turn/end`, excluding the in-flight turn entirely.
|
||||
*
|
||||
* Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default.
|
||||
*
|
||||
* `ctx.subagents` that runs each child as a child {@link Agent} SEEDED with a prefix of the
|
||||
* parent's session log — so the child inherits the parent's conversation context instead of
|
||||
* starting fresh. The seed ends at the last `turn/end`: the current tool-call turn is
|
||||
* unbalanced and cannot be replayed as a valid child session.
|
||||
* @module @deepseek-ai/dsh-subagent-fork
|
||||
*/
|
||||
|
||||
@@ -45,12 +32,10 @@ export const Config: z<Config> = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* The balanced completed-turn prefix of `parent`'s log: every event up to and
|
||||
* including the last `turn/end`. Empty if the parent has never completed a turn
|
||||
* (the in-flight turn is excluded, so a parent on its very first turn forks an
|
||||
* empty — i.e. fresh — child). The result is contiguous from seq 0 (the live
|
||||
* log keeps `seq === index`), so it is a valid session seed; the in-flight,
|
||||
* unbalanced turn is dropped so the invariants replay accepts it.
|
||||
* The balanced completed-turn prefix of `parent`'s log: every event up to and including the
|
||||
* last `turn/end`. The in-flight turn is excluded; before any completed turn the child starts
|
||||
* fresh. Because live sequence numbers equal array indexes, the result remains a valid seed
|
||||
* beginning at sequence zero.
|
||||
* @param parent - the agent whose session log to slice.
|
||||
* @returns the seed events, contiguous from seq 0; empty when no turn has completed.
|
||||
*/
|
||||
|
||||
@@ -136,10 +136,9 @@ describe('dsh-subagent-fork', () => {
|
||||
})
|
||||
|
||||
it('produces an invariant-CLEAN seed: forking mid-turn excludes the open turn', async () => {
|
||||
// Drive the parent so it has ONE completed turn, then start a SECOND turn
|
||||
// that is still open (a hanging model call), and fork while it's in flight.
|
||||
// The fork must seed only the completed first turn — an unbalanced seed
|
||||
// would make the invariants replay throw inside ctx.subagents.start.
|
||||
// Drive the parent so it has one completed turn, then start a SECOND turn that is still
|
||||
// open (a hanging model call), and fork while it's in flight. The seed must stop after the
|
||||
// balanced first turn; including the open turn would fail invariant replay during start.
|
||||
const { ctx, parent } = await setup([textResponse('done'), 'hang', textResponse('child')])
|
||||
parent.send([{ type: 'text', text: 'q1' }])
|
||||
await parent.whenIdle()
|
||||
@@ -184,12 +183,8 @@ describe('dsh-subagent-fork', () => {
|
||||
})
|
||||
|
||||
it('does NOT return the seeded parent output when the child produces no message of its own', async () => {
|
||||
// Regression: readResult must scope to the child's OWN events (after the
|
||||
// seed). The parent completes a turn with a distinctive assistant message,
|
||||
// then the fork child's own turn finishes with a bare `stop` and NO
|
||||
// assistant/message. Scanning the whole (seeded) log would return the
|
||||
// parent's "parent stale" message with stopReason 'completed'; scoped to the
|
||||
// child's own events the output is empty.
|
||||
// `readResult` must scan only child-owned events after the seed. The child emits no assistant
|
||||
// message, so scanning the whole log would incorrectly return the parent's distinctive text.
|
||||
const { ctx, parent } = await setup([textResponse('parent stale'), emptyStop])
|
||||
parent.send([{ type: 'text', text: 'parent question' }])
|
||||
await parent.whenIdle()
|
||||
|
||||
Reference in New Issue
Block a user