Commit Graph

402 Commits

Author SHA1 Message Date
Tianyi Cui
413db68008 Clarify the child-ordering invariant (Codex review follow-up)
The createdAt+recordedId child sort comment over-claimed "tie-safe". Codex
flagged that a same-millisecond sibling tie would be broken by random session
id, which does not recover first-call order. In the current synchronous cut that
tie is unreachable — the subagent tool awaits one child's result and disposes it
before the parent starts the next, so siblings' createdAt values are strictly
ordered and match first-call order. Restate the comment to that real invariant
(at both the replay sort and the harvest sort), note that the id tiebreak only
makes a degenerate collision deterministic, and flag the concurrent-subagent cut
that would need a real first-call ordinal with XXX(concurrent-subagents). The RFC
records the same limitation. Comment/doc only — no behavior change.
2026-06-22 09:01:09 +08:00
Tianyi Cui
e68496fd79 Add per-session snapshot replay for nested agents (PR2.5)
The snapshot tier was built single-session: dsh-llm-replay served calls from
one global positional cursor, and the harness harvested one session log. A
subagent runs as a second agent with its own session, so a parent→child
scenario could neither replay deterministically nor harvest the child's log.
This resolves the TODO(subagent-snapshots) deferral from the subagent RFC.

- Stamp the calling session id onto the model request: GenerateOptions.sessionId
  (typed Branded<'SessionId'> to avoid the dsh-llm↔dsh-session cycle), set by the
  agent loop from agent.session.id. Adapters ignore it; an llm/stream listener
  routes by it.
- Key replay per session: dsh-llm-replay loads the parent log plus one per child
  (childFiles / $DSH_SNAPSHOT_CHILD_FILES), derives a script per recorded session,
  and binds each live (freshly-random) session to a recorded script by first-call
  order — parent first (earliest createdAt, first to stream). Keys by WHO calls,
  so it survives a future concurrent/backgrounded subagent; a global cursor would
  not. An unrecorded extra session fails loud.
- Harvest every log: the harness collects all .jsonl across cwd buckets, ordered
  primary-first (top-level, then children by createdAt), and RunResult exposes the
  plural sessionLogs. The spec writes each back on record (session.jsonl +
  session.<n>.jsonl) and diffs each against its fixture on replay.
- Wire the subagent seam + spawn + fork + tool into the acp-agent example (both
  cordis configs) and add two nested scenarios recorded against the real API:
  subagent-spawn (parent + 1 child) and subagent-multi (parent + 2 children, 3
  sessions). Both replay keyless in the default gate.

A new RFC documents the design (docs/rfc/implemented/testing/). Single-session
replay is unchanged (a call with no sessionId is one anonymous primary session).

TODO follow-up: a dedicated branded-ids package could own the SessionId brand and
dissolve the cross-package cycle note; out of scope for this testing PR.
2026-06-22 08:39:36 +08:00
Tianyi Cui
9c1048f2b5 Honor an already-aborted request signal in the subagent driver (Codex review round 2)
A request signal aborted BEFORE the run starts never fires an `abort` event
(`addEventListener` only fires on the transition), so the backend-level bridge
missed it and ran the child to `completed`. The driver now checks
`request.signal?.aborted` at the top of the result path and settles `aborted`
without running the child. Regression test proven red on the pre-fix code.

Also refresh two stale RFC prose blocks the round-1 fix left behind: the
subagent RFC's Problem statement (cited the removed `TODO(sub-agents)` markers
and claimed nothing existed yet) and the unify-id RFC's fork/spawn risk bullet
(described the seam as "explicitly deferred" via `AgentLoop.create`'s old TODO),
now pointing at the realized seam.
2026-06-22 07:32:09 +08:00
Tianyi Cui
b82c310db3 Fix subagent in-process result scoping (Codex review round 1)
Two merge-blocking bugs in the shared in-process run driver, both rooted in
`readResult` scanning the whole child session and deriving the stop reason only
from `turn/end`:

- A pre-turn `cancel()` cleared the queued prompt before any `turn/end` was
  logged, so the run settled `error` instead of `aborted`, violating the
  `SubagentRun.cancel()` contract. The driver now tracks that a cancel was
  requested and maps the no-turn case to `aborted`.
- A fork child whose own turn produced no `assistant/message` returned the
  SEEDED parent's last message as a `completed` success. `readResult` now scopes
  to the child's OWN events (after the seed prefix), so a message-less child
  yields empty output.

Both fixes carry a regression test proven to go red on the pre-fix driver.

Also: correct the `SubagentRun.id` / event-payload docs (it is the child AGENT
id, not a session id — the backend mints distinct tokens); refresh the stale
`coding-agent` welcome string (subagent is now a tool); and replace the stale
`TODO(sub-agents)` "deferred" prose in the Agent interface, core.md, and
architecture.md with an accurate pointer to the realized seam.
2026-06-22 06:47:20 +08:00
Tianyi Cui
7aabd2a3df Add in-process subagent backends: spawn (fresh) and fork (seeded)
The second PR of the subagent seam: the two in-process backends that run a
child agent on the same cordis context, reusing the agent factory's quiescent
AgentHandle teardown. Both register on ctx.subagents (PR1's named-provider
registry) and share one run driver.

- dsh-subagent-spawn: a FRESH child via ctx.agents.create — own session, the
  parent's model by default (overridable), zero inherited conversation. Also
  exports the shared in-process run driver (startInProcessRun): mint ids, stamp
  cwd/parentSession-lineage/depth, drive the one-shot (send → whenIdle), read
  the last assistant/message + turn/end reason, dispose to quiescence.
- dsh-subagent-fork: a child SEEDED with the parent's balanced completed-turn
  prefix (the log up to and including its last turn/end), so the child inherits
  context. The in-flight unbalanced turn is excluded — a raw seed would fail the
  invariants replay. Proven: a regression test goes red if the boundary seeds
  the open turn.
- Seam extension: CreateAgentOptions.seed, threaded through AgentLoop.createAgent
  → ctx.sessions.prepare({ seed }) (the primitive resume already used). This is
  the fork-lineage path the TODO(sub-agents) markers anticipated.
- Depth: a merge-extensible AgentOptions.subagentDepth (0 top-level, parent+1 for
  a child); the depthLimit capability refuses a spawn past request.maxDepth.

Tests: real-loop unit tests for both backends (mock MODEL only, real loop +
invariants), a multi-subagent test (one parent drives a fork AND a spawn child
then keeps working), and a with-key e2e (a real parent delegates via the
`subagent` tool to a real child that writes a file on disk — world-verified).
100% per-file coverage. The coding-agent demo wires the spawn backend + tool.

Snapshot coverage of nested agents is deferred to a stacked follow-up
(TODO(subagent-snapshots)): dsh-llm-replay is a single global positional cursor
that cannot route calls to a parent vs. a child on one context. Recorded in the
RFC's deferrals and a new AGENTS.md rule: designing a subsystem must design its
test infrastructure END TO END up front, verifying the snapshot/e2e harness can
express the new shape — a gap this plan hit.
2026-06-22 05:58:40 +08:00
Tianyi Cui
861791d2d8 Contain subagent lifecycle listeners per-listener, not per-emit
A single try/catch around ctx.emit prevented a thrown subagent/start or
subagent/end listener from propagating, but cordis emit dispatches listeners in
a `.map(cb => cb())` that HALTS on the first throw — so a bad subscriber still
starved the listeners registered after it, violating the AGENTS.md
callback-boundary rule ("one bad subscriber must not starve the listeners after
it"). Resolve the listener callbacks via ctx.events.dispatch and contain each
call individually, the same per-listener guarantee BashExecutor.notifyTaskDone
gives its own listener set.

The two containment tests now register TWO listeners where the first throws and
assert the second still observes the event (start) and the settle (end) — a
regression that fails on the per-emit code (verified: reverted, watched both go
red, restored).
2026-06-21 23:44:07 +08:00
Tianyi Cui
25eccdaedc Fix review findings: lifecycle containment, configurable tool name, coverage, type catalog
Address four findings from the first Codex review round:

- Contain subagent/start|end listener throws (emitContainedStart/End): a
  thrown lifecycle listener could escape SubagentService.start() before the
  caller received the live run to dispose it (a leaked child), and a thrown
  subagent/end listener could surface as an unhandled rejection on the detached
  result-settle hook. Both emits now log-and-contain, mirroring the agent
  registry's agent/created|disposed containment.
- Make the model-facing tool name configurable (Config.toolName, default
  subagent). The docs say to load dsh-tool-subagent once per provider to expose
  multiple transports, but the hardcoded name made the second load throw a
  duplicate-tool-name error; a distinct toolName per load is now required and
  documented.
- Reach the per-file 100% coverage gate: tests for the subagent/end error
  branch, lifecycle-listener containment, every stopReasonError arm + the
  merge-extensible default, the multi-provider toolName path, agentOptions
  forwarding, and the direct-apply schema-bypass fallbacks.
- Document the seam vocabulary in docs/core-data-structures/subagent.md with
  verbatim type-equiv blocks + manifest entries, and link it from core.md (a
  brand-new core/seam type the doc-sync gate cannot detect on its own).
2026-06-21 23:15:43 +08:00
Tianyi Cui
1a81f2cccd Add subagent capability seam: interface, mock backend, model-facing tool
Introduce the `packages/subagent/` group and the abstract subagent seam — an
agent delegating to a child agent — as a named-provider registry (`ctx.subagents`),
unlike the single-implementation bash seam, so multiple transports (in-process,
ACP, future A2A) coexist. This first PR lands the interface, a scripted test
backend, and the model-facing tool, validated through the real cordis load path.

- dsh-subagent: SubagentService registry + SubagentProvider/SubagentRun
  vocabulary + subagent/start|end events. Start-time capabilities (outputSchema,
  depthLimit, toolFilter) are checked pre-start and rejected loud; runtime
  capabilities (sendMessage, resume) are optional methods on SubagentRun.
- dsh-subagent-mock (support): scripted provider for keyless, deterministic
  tests through the real Loader/export path.
- dsh-tool-subagent: the model-facing `subagent` tool, config-bound to one
  provider; synchronous collect with try/finally dispose, signal->cancel
  bridging, and non-completed-stop-reason -> isError mapping.
- Proposed RFC documenting the seam, the fork-vs-spawn-as-separate-backends
  decision, own-session isolation, synchronous-collect scope, and the deferral
  of background/poll/spill to a future unification with bash.
- Wire the new group into tsconfigs, build refs, package hierarchy docs, the
  module graph, and the cordis catalog.

RFC: docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md
2026-06-21 22:31:56 +08:00
Tianyi Cui
6b4dc48fbd Merge pull request #89 from deepseek-ai/worktree-lessons-learned
docs(agents): stacked-PR orchestration + sharper real-entry-path lessons
2026-06-21 20:38:31 +08:00
Tianyi Cui
a20e3b70b9 Merge remote-tracking branch 'origin/master' into worktree-lessons-learned 2026-06-21 20:37:26 +08:00
Tianyi Cui
5d3e7dd590 Merge pull request #88 from deepseek-ai/worktree-simplify-extract-examples
refactor(examples): extract the app spine into dsh-agent-core + app packages
2026-06-21 20:37:01 +08:00
Tianyi Cui
0b75598c91 Merge remote-tracking branch 'origin/master' into worktree-simplify-extract-examples 2026-06-21 20:36:05 +08:00
Tianyi Cui
7373941493 Merge pull request #87 from deepseek-ai/worktree-simplify-snapshot-goldens
test(snapshot): use session.jsonl as the only session-log artifact
2026-06-21 20:35:41 +08:00
Tianyi Cui
c85713d11e Merge remote-tracking branch 'origin/master' into worktree-simplify-snapshot-goldens 2026-06-21 20:34:55 +08:00
Tianyi Cui
e770e22486 Merge pull request #86 from deepseek-ai/worktree-simplify-trace-events
simplify(session): fold trace-only usage/error events into load-bearing events
2026-06-21 20:34:41 +08:00
Tianyi Cui
5d7af3be82 Merge remote-tracking branch 'origin/master' into worktree-simplify-trace-events 2026-06-21 20:33:53 +08:00
Tianyi Cui
9f13dd02f6 Merge pull request #85 from deepseek-ai/worktree-simplify-agent-stop
simplify(agent): drop unused public Agent.abort(), keep whenIdle()
2026-06-21 20:33:27 +08:00
Tianyi Cui
4ead67c06d Merge remote-tracking branch 'origin/master' into worktree-simplify-agent-stop 2026-06-21 20:32:21 +08:00
Tianyi Cui
8f50008d8b Merge pull request #84 from deepseek-ai/worktree-simplify-branded-ids
feat(types): brand the bash ids and stop brand erosion
2026-06-21 20:32:05 +08:00
Tianyi Cui
74344a8bd4 Merge remote-tracking branch 'origin/master' into worktree-simplify-branded-ids 2026-06-21 20:30:41 +08:00
Tianyi Cui
2d4c367e51 Merge pull request #83 from deepseek-ai/worktree-simplify-prune-seam
simplify(seams): prune dead methods from the persistence seam
2026-06-21 20:30:14 +08:00
Tianyi Cui
fa5cf0afd7 Merge remote-tracking branch 'origin/master' into worktree-simplify-prune-seam 2026-06-21 20:27:19 +08:00
Tianyi Cui
89d9a6679b Merge pull request #82 from deepseek-ai/worktree-simplify-llm-surface
simplify(llm): drop unconsumed adapter-change event and assembled call surfaces
2026-06-21 20:27:04 +08:00
Tianyi Cui
df4f8a4dea Merge remote-tracking branch 'origin/worktree-simplify-extract-examples' into worktree-lessons-learned 2026-06-21 20:22:47 +08:00
Tianyi Cui
6cb8f35adb Merge remote-tracking branch 'origin/worktree-simplify-snapshot-goldens' into worktree-simplify-extract-examples
# Conflicts:
#	AGENTS.md
2026-06-21 20:21:37 +08:00
Tianyi Cui
56a2351e08 Merge remote-tracking branch 'origin/worktree-simplify-trace-events' into worktree-simplify-snapshot-goldens 2026-06-21 20:19:16 +08:00
Tianyi Cui
fd76791c6b Merge remote-tracking branch 'origin/worktree-simplify-agent-stop' into worktree-simplify-trace-events 2026-06-21 20:18:15 +08:00
Tianyi Cui
a6a5892e8a Merge remote-tracking branch 'origin/worktree-simplify-branded-ids' into worktree-simplify-agent-stop
# Conflicts:
#	docs/rfc/README.md
2026-06-21 20:17:29 +08:00
Tianyi Cui
4661905714 Merge remote-tracking branch 'origin/worktree-simplify-prune-seam' into worktree-simplify-branded-ids
# Conflicts:
#	docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
2026-06-21 20:15:21 +08:00
Tianyi Cui
5c0597e9de Merge branch 'worktree-simplify-extract-examples' into worktree-lessons-learned 2026-06-21 19:48:37 +08:00
Tianyi Cui
4d7726ecd3 fix review findings: don't treat disabled entries as load failures; scope the verify-package-paths lib skip to a real package root
assertEntriesLoaded() flagged ANY fiber-less entry as a failed import, but a
`disabled: true` entry settles without a fiber by design (Entry.refresh() skips
init() when disabled) — a valid "plugin off" config, not a broken import. Both
app bins now filter `fiber === undefined && !entry.disabled`. The stdio built-bin
smoke gains a disabled-(unresolvable)-entry config that must still boot.

The verify-package-paths lib-skip was unconditional and ran before the
moved-package check, so a stale group-less `packages/acp-agent/lib/bin.js` (the
exact drift this gate catches) was silently ignored just for containing `lib`.
Scope the skip: only exempt `lib` when it is the segment after an EXISTING
`packages/<group>/<pkg>` root, so a real-but-unbuilt `lib/bin.js` is still exempt
while a stale package path flags.
2026-06-21 19:47:22 +08:00
Tianyi Cui
64976df216 Merge branch 'worktree-simplify-extract-examples' into worktree-lessons-learned 2026-06-21 18:38:56 +08:00
Tianyi Cui
9f5e2b352c Merge branch 'worktree-simplify-snapshot-goldens' into worktree-simplify-extract-examples 2026-06-21 18:38:50 +08:00
Tianyi Cui
ebe8c8e21f Merge branch 'worktree-simplify-trace-events' into worktree-simplify-snapshot-goldens 2026-06-21 18:38:40 +08:00
Tianyi Cui
f09a88ecee Merge branch 'worktree-simplify-agent-stop' into worktree-simplify-trace-events 2026-06-21 18:38:29 +08:00
Tianyi Cui
2903f96548 fix review findings: RFC says session.jsonl is required for every snapshot scenario
The required-fixture-guard description still said session.jsonl was needed only
for model scenarios, but the harness passes <dir>/session.jsonl to llm-replay
unconditionally, so loadReplayScript() fails for a no-model scenario without it.
The code already requires it for all scenarios; align the RFC prose.
2026-06-21 18:36:13 +08:00
Tianyi Cui
d0e1b02a7f fix review findings: correct whenIdle live-consumer claim in the stop-surface RFC
The retained-whenIdle paragraph claimed "live consumers (the ACP bridge's settle
points)", but `packages/ui/acp/src` has no whenIdle() call — the bridge owns its
agents and tears them down via AgentHandle.dispose(). whenIdle()'s live consumers
are ACP and agent TESTS awaiting settlement through the public seam. State that.
2026-06-21 18:36:04 +08:00
Tianyi Cui
b7d018580e fix review findings: skip lib/ build-output refs in verify-package-paths; resolve acp built-bin npm deps from the declaring package
verify-package-paths flagged the new built-bin smokes' `lib/bin.js` citations
as stale-source drift, failing CI: doc-sync runs BEFORE build, so the build
output is absent at lint time. The gate targets moved SOURCE paths, so skip any
reference whose target goes through a `lib/` segment — mirroring how the file
scan already excludes `lib/`.

The acp built-bin smoke resolved `zod`/`@agentclientprotocol/sdk` via
`import.meta.resolve` from the test file's own context, but `acp-agent` does not
declare them — `dsh-acp` does. Under pnpm's strict layout they are not exposed
where the test resolves, so the new CI built-bin step failed with
"Cannot find package 'zod'". Resolve each from the `ui/acp` package URL (the one
that declares it) instead.
2026-06-21 18:35:46 +08:00
Tianyi Cui
a96b1e3ae7 docs(agents): capture stacked-PR orchestration + sharper real-entry-path lessons
Three hard-won lessons from running a 7-PR simplification stack through two
waves of review feedback:

- New "## Orchestrating review feedback across a stacked PR chain" section: one
  worktree per branch; a fix belongs on the PR that introduced the issue then
  flows DOWN; review-fixes are separate commits never amends; delegated work is
  trust-but-verify (prove a regression guard FAILS on unfixed code); reply
  in-thread on the merits.
- "## Conventions" gains a "Never rewrite a pushed branch" rule next to the
  merge-commit rule: update a child by merging the parent down, never
  rebase/amend/force-push a pushed branch; a fix lands on its originating PR.
- Extended the "Line coverage is not behavior coverage" defensive-patterns
  bullet with two corollaries this stack re-taught: (1) a real-load-path test
  only GUARDS the export shape if a broken shape actually FAILS it — an
  inject-less composition plugin boots fine on a stray `export default`, so it
  needs an explicit no-default + unwrapExports assertion; (2) "real entry path"
  means the PUBLISHED artifact (built lib/bin.js under plain node), not the dev
  runtime (tsx), which masks boot settle-races, module-resolution differences,
  and a load failure that loader.await()'s Promise.allSettled swallows.
2026-06-21 17:17:00 +08:00
Tianyi Cui
e63641bcfd Merge branch 'worktree-simplify-snapshot-goldens' into worktree-simplify-extract-examples 2026-06-21 15:48:33 +08:00
Tianyi Cui
e04ca55374 fix review findings: require session.jsonl for every snapshot scenario
The per-kind fixture guard claimed no-model scenarios need no session.jsonl, but
the replay path requires one for ALL scenarios: runScenario() passes
`fixtureFile: <dir>/session.jsonl` unconditionally and llm-replay's
loadReplayScript() throws "fixture not found" when it is absent and no override
replaces it. A no-model scenario ships a header-only session.jsonl that derives
to an empty script. The guard + its comment now match that reality, so a future
no-model scenario following them won't fail at subprocess startup.
2026-06-21 15:46:38 +08:00
Tianyi Cui
3567808171 fix review findings: harden the app bins + built-bin smokes, arch-exception doc, snapshot fixture-guard
BLOCKER — the published lib/bin.js (stdio + acp) was exercised only via tsx
(demo:* / the src/bin.ts smokes); the built artifact under plain `node` was
unguarded. Root-cause on the BUILT bin:
  1. Settle race: boot() returned once loader.create() registered the include
     ENTRY, but the include loads its child plugins asynchronously — so boot()
     (and main()) resolved while the app plugins (stdin reader, agent loop, ACP
     bridge) were still mounting. A CLI with no attached handles yet exits 0
     silently, and a load error surfaces as an unhandled rejection AFTER boot.
     Fix: `await ctx.loader.await()` after create() — settle the whole tree.
  2. Config-path robustness: hand the include the config's ABSOLUTE file:// URL
     so resolution never depends on ctx.baseUrl / can never fall back to cwd.
Both bins fixed identically. NOTE: the cordis Loader resolves a config's bare
plugin specifiers via its internal module loader, active only under
`node --expose-internals`; the bin cannot add a node flag itself, so this is
documented in the bin JSDoc + both package READMEs (the demos already comply).
The repo `examples/*/cordis.yml` are tsx-only artifacts (workspace plugins
resolve through the tsconfig paths map, not node_modules), so they are not a
valid plain-node bin target — the smokes use a real-install-shaped temp dir.

Fail loud on a load failure: boot() previously exited 0 SILENTLY when a config
path's directory does not exist — the include plugin fails to IMPORT, the cordis
Loader catches+LOGS it and leaves the entry with no fiber (no rejection), and
`loader.await()` does not rethrow (EntryTree.await uses Promise.allSettled). Fix:
boot() now calls assertEntriesLoaded(ctx) after the tree settles and throws on
any entry with no fiber, so a typo'd config dir exits non-zero with a clear
message. main() also installs an unhandledRejection guard (installFailLoud) that
replaces Node's stack dump with a single labelled stderr line for the
companion case (a missing config FILE in a real dir, whose include-init throw
surfaces as a rejection Node already exits non-zero on). Regression tests added
to both built-bin smokes (missing dir + missing file → non-zero exit + stderr);
verified the missing-dir test fails on the pre-fix bin.

Built-bin smokes (the reviewer's ask): packages/ui/{stdio,acp}-agent/tests/
built-bin.e2e.ts run the REAL lib/bin.js under `node` (NOT tsx) in a temp
consumer dir, asserting the stdio echo round-trip / the acp initialize response
+ stdout purity, plus the fail-loud cases above. They build-gate (skip if lib/
absent) and run in a new ci.yml step after the build.

Issue 2 — packages/README.md + docs/architecture.md said "plugins depend on
interfaces, never on the concrete loop", but dsh-agent-core imports the concrete
dsh-agent-loop. Scope the rule to EXTENSION plugins and carve out the sanctioned
COMPOSITION/bundle exception (dsh-agent-core composes the concrete spine); note
it in the implemented RFC too.

Issue 3 — examples/acp-agent/tests/acp.snapshot.ts fixture-guard claimed
no-model scenarios need no session.jsonl, but runScenario() always boots
llm-replay with the session.jsonl path and loadReplayScript() throws when it is
absent. Require session.jsonl for ALL scenarios (no-model ones ship a
header-only fixture) and rewrite the comment to match reality.
2026-06-21 15:39:24 +08:00
Tianyi Cui
9e86fd995c fix(ci): repoint the demo smoke test at the dsh-stdio-agent bin
The CI "Demo smoke test" step booted the deleted examples/echo-agent/start.ts
(removed when the boot glue moved into the dsh-stdio-agent bin), so CI failed on
node 24 + 26 while the local gates and e2e passed. Invoke `pnpm run demo:echo`
instead of hardcoding the boot path — that routes through the canonical demo
script, so the smoke can never drift from it again. The output assertions and
the .sessions/_no-cwd/main-session-*.jsonl artifact check are unchanged
(verified the new bin produces identical output).
2026-06-21 14:23:19 +08:00
Tianyi Cui
bfc5a9097d fix review findings: soften the last stdout-purity overclaim in the acp-agent module doc
The acp-agent README/RFC overclaim was already softened; this module doc comment
still said the footgun is "structurally unreachable from the leaf". A leaf
cordis.yml CAN still add a sibling @cordisjs/plugin-logger-console — the app does
not prevent it. Reword to the accurate claim: the app gives the default front
door no logger entry to misconfigure, and the "never add a stdout logger to an
ACP leaf" rule still stands.
2026-06-21 13:08:11 +08:00
Tianyi Cui
66e56bd395 fix review findings: stale coding-agent README, export-shape guards, overclaim wording
Codex review of PR #88 found three issues in the example-app extraction:

A1 — examples/coding-agent/README.md's plugin table still listed the OLD
direct-wired leaf entries (agent-loop, session-persistence, src/stdio-chat.ts —
the whole src/ dir is gone). Rewrite it to the four real leaf entries the
current cordis.yml loads (hmr, llm-deepseek, bash, stdio-agent), noting that
tool-bash/persistence/agent/loop now live inside the agent-core + stdio-agent
bundles.

A2 — the three new app/spine packages (agent-core, stdio-agent, acp-agent)
export NO `inject`, so a stray `export default apply` would let unwrapExports
collapse the module and silently DROP name/Config WITHOUT crashing — the
real-load-path smokes would stay green. agent-core is never Loader-unwrapped at
all. Add an explicit export-shape guard per package: assert no `default` export
and that the real Loader.unwrapExports leaves name/Config/apply intact. Verified
each fails when `export default apply` is added.

B — soften "structurally unreachable / cannot wire a stdout logger" overclaims
in the acp-agent/agent-core READMEs and the implemented RFC: a leaf CAN still add
a sibling logger entry; the accurate claim is the app omits one so the default
leaf has nothing to get wrong. Keep the safety directive (never add a stdout
logger to an ACP leaf).
2026-06-21 12:49:59 +08:00
Tianyi Cui
47afcb297f Merge PR6 (snapshot golden removal + PR5 trace-events) into PR7 2026-06-21 12:10:49 +08:00
Tianyi Cui
e2bde2902c refactor(examples): extract the app spine into dsh-agent-core + app packages
Implements docs/rfc/.../2026-06-20-extract-example-app-packages.md. Each
example was thick — a hand-rolled start.ts, an infra preamble, nested
base.yml/base-core.yml/acp-tail.yml includes, and a coupled front-door
cluster enforced only by prose. This moves the composition into packages so
each example is a thin leaf cordis.yml: pick the swappable backends, load one
app package.

New packages:
  - @deepseek-ai/dsh-agent-core (packages/core/agent-core): one bundle plugin
    that loads the providerless/executor-less/UI-less spine (timer + llm +
    sessions + system-prompt + tools + agents + invariants + tool-bash +
    agent-loop) via ctx.plugin(...) inside apply(), and forwards agent-loop's
    `agents` list as its own Config (export const Config = AgentLoop.Config,
    default []).
  - @deepseek-ai/dsh-stdio-agent (packages/ui/stdio-agent): terminal chat APP —
    agent-core + console logger + readline UI + a pre-created `main` agent, with
    a bin. The demo:echo/coding front door.
  - @deepseek-ai/dsh-acp-agent (packages/ui/acp-agent): ACP server APP —
    agent-core + JSONL persistence + the acp bridge, NO stdout logger, with a
    bin. The stdout-purity footgun is structurally unreachable from the leaf.

Amendment to the RFC: hmr stays a LEAF cordis.yml entry, not baked into
dsh-stdio-agent. hmr is a Loader-only dev plugin (throws without
--expose-internals; the in-process test tier can't even import its decorator
form), so a package statically importing it could never carry the per-file
coverage gate. Unlike the console logger, a stray hmr is not a stdout-purity
footgun, so leaving it at the leaf costs no safety. With hmr out, all three new
packages carry in-process unit specs at 100%.

Boot glue (Loader tail, .env load, snapshot-mode selection, stdin-dispose
lifecycle) moves into each app's bin; start.ts and base.yml/base-core.yml/
acp-tail.yml are deleted. Each app package gets a keyless real-load-path test
that boots through its bin + the cordis Loader (guarding the unwrapExports
export-shape bug class, postmortem 0001). ACP snapshot replay stays green
against the existing committed goldens (pure boot restructuring). RFC moved
proposed->implemented with the amendment recorded; package/example/architecture
docs and the module graph updated.
2026-06-21 12:06:38 +08:00
Tianyi Cui
ed2e230f20 Merge PR5 doc-sync fixes (b4471e5) into PR6 2026-06-21 11:34:46 +08:00
Tianyi Cui
2eb6ad3260 fix review findings: sync stale v1/removed-event doc references
Codex's re-confirmation pass verified both blocker fixes correct but found
doc/comment drift the fix commit missed:

- session/index.ts + session/README.md: "minimal v1 header" → "minimal header
  (stamped with the current SESSION_FORMAT_VERSION)" — the version is 0, not 1.
- session/index.ts deriveMessages comment listed "usage, and errors" as trace
  data — those standalone events no longer exist; only boundaries + chunks are.
- session-persistence RFC: "no v1 migration" → the pinned-v0 pre-release stance.
- collapse-trace-only RFC format-version note: reframed off the "bump the
  version and reject" wording (which now reads as the OTHER AGENTS.md stance)
  onto the pinned-0 unstable stance the session log actually uses.
- agent-loop/loop.ts finishError JSDoc: "with a logged `error` event" → the
  failure is recorded on turn/end.reason (no standalone error event).
- acp/acp-feature-support.md (two spots): usage is recorded on assistant/message
  now, not as standalone internal usage events.
- Regenerate the cordis catalog (finishError JSDoc line shift).
2026-06-21 11:33:41 +08:00
Tianyi Cui
655206848d Merge PR5 (trace-event collapse + v0 format pin) into PR6
# Conflicts:
#	examples/acp-agent/tests/snapshots/cancel/session.golden.jsonl
#	examples/acp-agent/tests/snapshots/cancel/session.jsonl
#	examples/acp-agent/tests/snapshots/error-finish/session.golden.jsonl
#	examples/acp-agent/tests/snapshots/error-finish/session.jsonl
#	examples/acp-agent/tests/snapshots/multi-turn/session.golden.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.golden.jsonl
#	examples/acp-agent/tests/snapshots/tool-call-turn/session.golden.jsonl
#	examples/acp-agent/tests/snapshots/workspace-edit/session.golden.jsonl
2026-06-21 11:12:48 +08:00