Commit Graph

17 Commits

Author SHA1 Message Date
Tianyi Cui
475c68cbe8 docs: sync package cookbook with build config 2026-06-22 11:36:35 +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
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
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
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
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
83e97ed222 fix review findings: document the util/ group + align branded-ids RFC with dsh-brand
Adding packages/util/brand/ created a new top-level packages/util/ group that
the hierarchy/dependency docs never enumerated. Document it:

- Add packages/util/README.md, the group README (low-level zero-dependency
  utilities shared across groups; lists dsh-brand).
- packages/README.md: add the util/ group to the group table, dsh-brand to the
  package table, and dsh-brand to the dependency graph. Correct the now-false
  "no harness deps" claims — dsh-llm and dsh-bash both depend on dsh-brand
  (verified dsh-bash imports Branded from dsh-brand, not dsh-llm; dsh-session
  and dsh-agent depend on it too).
- Root AGENTS.md Repository Layout: add the util/ group with brand/.

Align the implemented branded-ids RFC with what shipped: Branded lives in
@deepseek-ai/dsh-brand (packages/util/brand/), and dsh-bash depends only on
that utility package instead of dsh-llm. Fix the BashTaskId import source, the
illustrative snippet, and the opening policy reference (now dsh-brand).
2026-06-21 11:04:28 +08:00
Tianyi Cui
f6bd1468f2 simplify(agent): drop the unused public Agent.abort(), keep whenIdle()
The public Agent handle exposed abort() (step-only) and cancel() (queue-aware).
No production caller used abort() — ACP maps session/cancel to cancel(), and
lifecycle owners tear down via AgentHandle.dispose(); the loop's own stop paths
abort their per-step AbortController directly. So abort() is latent generality
that keeps a private loop mechanic public.

RFC-premise correction: the public-agent-stop-surface RFC proposed removing
whenIdle() too. Implementation found whenIdle() load-bearing — a real
quiescence primitive with a deliberate loop contract (settle-without-transition,
the replacement-turn race) and ACP test consumers; its proposed replacement
("observe the running->idle transition") is exactly the async-state race
AGENTS.md warns against. So only abort() is removed; whenIdle() stays. The RFC
is amended on the way to implemented/ to record the narrowed scope, and the new
AGENTS.md "RFCs are proposals, not golden truth" principle (PR1) gets its
worked example.

- Remove Agent.abort() from the interface + the ReactLoopAgent impl; the no-arg
  'aborted' default goes with it (cancel() keeps its 'cancelled' default).
- Migrate tests: empty-queue abort() -> cancel(reason); the two review-fixes
  tests whose subject is the in-flight step's AbortController drive that
  controller directly via the private currentAbort field (cancel() would clear
  the inbox and destroy the queued steering one of them proves survives a step
  abort). The no-arg-default test is dropped (cancel()'s default is already
  covered in cancel.spec.ts).
- Resulting public stop surface: cancel() + whenIdle(). Update agent/agent-loop
  READMEs, architecture.md, core.md type-equiv, the extension cookbook, the
  lifecycle RFC (short note), and the proposed ACP RFC.

Implements docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md
2026-06-21 09:05:21 +08:00
Tianyi Cui
d6a2ab30c8 feat(types): brand bash ids + stop brand erosion; extract Branded to dsh-brand
Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes
the two gaps in the "brand ids that cross package boundaries" policy and fixes
the dependency direction so a capability package never pulls in an unrelated one.

- Extract the `Branded<B>` primitive into a new standalone type-only package
  `@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps.
  dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session,
  dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on
  dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a
  generic execution backend must not couple to the LLM or session vocabulary).
- Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id,
  the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and
  the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from
  SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary
  that casts SessionId -> OwnerToken.
- Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types
  agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters
  at the config boundary and the inner create()/resume casts disappear (only the
  genuinely-new per-run session-id string is cast).
- Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store
  Map keys and public params/exports (SessionStore, AgentRegistry + factory
  options, the ACP session-id surface + ToolPresenter CallId map, the
  persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps).
- Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point
  the Branded type-equiv at dsh-brand, fix stale param types in the session/
  agent/bash READMEs, regenerate the cordis catalog + module graph.

Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
2026-06-21 07:19:59 +08:00
Tianyi Cui
7792347c4f simplify(seams): prune dead methods from the persistence and bash seams
Two capability seams carried abstract methods no production consumer calls.
A method no consumer programs against is not a seam — it is speculative
surface every implementation must still provide and test.

- SessionPersistence: remove has() and delete(), the coordinator's
  has/delete/deleteCore, and the PersistenceBackend.deleteStored hook (with its
  jsonl + sqlite + in-spec memory-stub impls). Surviving service surface:
  create/append/load/list. Production uses only load() (resume) and list()
  (ACP session/list).
- BashExecutor: remove get(id) and list(), the abstract decls and the
  LocalBashExecutor impls. The internal tasks map survives (it backs
  ownerOf/readOutput/kill); get/list were pure public accessors over it with no
  shipping caller and no bash_list tool.
- Migrate tests that reached through ctx.bash.get(id) to the public completion
  seam: a doneFor(id) helper over onTaskDone awaits a task by id, and the
  HMR-reload ownership test now proves task survival through A's own bash_output
  ([status: running]) plus ownerOf + B-rejection — a stronger through-the-tool
  assertion than the removed lookup peek.
- Update seam READMEs (six -> four service methods, drop the deleteStored hook
  and the get/list row) and the two implemented persistence RFCs in place.

Implements docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
2026-06-21 02:17:27 +08:00
Tianyi Cui
30cd67b8a1 simplify(llm): drop unconsumed adapter-change event and assembled call surfaces
The LLM service exposed three call surfaces (stream/streamBlocks/generate) but
the only production consumer — the agent loop — uses stream() exclusively,
feeding raw chunks through its own BlockAssembler for replay fidelity. Drop the
speculative convenience surfaces and the registry-change event that no listener
consumed, leaving stream() as the single model-call contract for both
production and tests.

- Remove LlmService.streamBlocks() and generate(), the llm/generate waterfall,
  and GenerateResult.
- Remove the llm/adapter-change event (declaration + emits) and the
  listener-throw rollback ordering that existed only to protect it; keep the
  HMR rollback disposer.
- Remove BlockAssembler.flushReady()/flushRemaining()/result() and the flushed
  cursor — the streaming-flush slice existed only for streamBlocks().
- Adapter tests drive a stream()+BlockAssembler helper (tests/assemble.ts)
  instead of generate(), exercising the same path production uses.
- Land the AGENTS.md "RFCs are proposals, not golden truth" principle and move
  both RFCs proposed -> implemented.

Implements:
- docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
- docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
2026-06-21 01:27:41 +08:00
Tianyi Cui
184066e745 Tighten implemented RFC after self-review
Self-audited every factual claim against the shipped branch (hierarchy
tree, no group manifests, the dsh-* paths wildcard in both tsconfigs,
publint glob, explicit build references, the two new gates). Phrase the
paths-wildcard bullet as current state, and record the doc-typecheck
JSONC-parse subtlety the wildcard introduced so a future editor does not
reintroduce the regex comment-strip bug.
2026-06-21 00:00:38 +08:00
Tianyi Cui
5024cd5757 Document the package hierarchy and finalize the RFC
Add a README to each group dir (core/llm/bash/session-persistence/ui/
support) stating its role and product-vs-support classification, and
rewrite packages/README.md around the hierarchy (group table, grouped
"what goes where", removed the package-hierarchy FIXME).

Move the package-hierarchy RFC to implemented/architecture/ and rewrite
it to describe what shipped (placement rationale, the paths-wildcard and
publint dedup, the two new guardrail gates). Fold the remaining
tsconfig.build.json references dedup into the discover-package-inventory
proposal and fix its cross-link.

Update AGENTS.md: regrouped repo-layout map, depth-2 globs, the new
verify-package-paths gate in the doc-sync listing, and a note that we
lean toward stricter lint in the agentic-coding era (machine-caught
errors and a consistent foundation outweigh the one-time cost).
2026-06-20 23:25:33 +08:00
Tianyi Cui
605587e79c docs(rfc): classify RFCs by kind via path-encoded subdirectories
Add a second axis to every RFC — its class (feature, bug-fix,
simplification, architecture, process, testing) — encoded in the path
as docs/rfc/{lifecycle}/{class}/file.md. The folder is the label, so
the closed set is enforced by structure rather than a parsed field.

Two new doc-sync gates back it:
- verify-rfc-classification: every RFC sits in a valid class folder and
  the README index lists it under the matching lifecycle→class heading.
- verify-doc-refs: every docs/*.md path cited in a packages|examples TS
  comment resolves — closes a drift class verify-md-links can't see, and
  catches the four comment refs this reorg moved.

The README gains a Classification section explaining the taxonomy and
per-class index sub-sections. A self-referential process RFC records why
the scheme is path-encoded and gated.
2026-06-20 22:29:45 +08:00