Commit Graph
10 Commits
Author SHA1 Message Date
Tianyi Cui 7c400e9c02 docs: unify ADR/RFC trees into one lifecycle-organized RFC tree
Collapse docs/adr/ and docs/rfc/ into a single docs/rfc/ with proposed/,
implemented/, and rejected/ subfolders. Every file is renamed to
yyyy-mm-dd-topic-title.md, where the date is when the topic was first
proposed (from git history). ADRs and RFCs that covered exactly the same
topic are merged (property-based testing, session persistence); the
umbrella RFC 005 stays split across its three implemented decisions, and
RFC 006's deferred part-3 (API extractor reports) splits into its own
proposed RFC. All cross-references become machine-checkable relative
links instead of bare "ADR NNNN" / "RFC NNN" prose.

Add a verify-md-links doc-sync gate (scripts/verify-md-links.ts) that
checks every relative Markdown cross-link resolves, wired into doc-sync
alongside verify-md-wrap. This makes the reorganization self-verifying:
the same change that rewrote ~forty inter-doc links adds the check that
proves none dangle. Document the cross-link convention in a new
docs/AGENTS.md and record the gate as an implemented RFC.

doc-sync, typecheck, lint, and the full test suite (667) all pass.
2026-06-18 02:18:24 +08:00
Tianyi Cui 2df41ee1d3 fix: make the six registration methods atomic under a throwing change-listener (P1-1)
llm.registerAdapter, agents.register, sessions.create, systemPrompt.section,
systemPrompt.tools, and tools.register each mutated state, emitted a change
event, then returned the disposer. In Cordis a synchronous throw before the
effect returns its disposer leaves nothing for the fiber to collect, so a
throwing change-listener leaked the registry entry permanently — HMR/dispose
could not clean it, and the duplicate-name/already-exists check stayed wedged
until restart.

Convert each to the generator-effect pattern already proven in
AgentLoop.create: mutate state, `yield` the disposer that undoes it (collected
before the next step runs, so it is torn down if a later step throws), THEN
emit the change event. The existing duplicate-name throws are unchanged — they
fire before any mutation, so they correctly leak nothing. No public API change:
generator effects are still synchronous SyncEffects and register() keeps
returning its fire-and-forget disposer wrapper.

Tests: a listener-throw rollback test for all six methods — register with a
change-listener that throws, assert the call throws AND the registry is clean
(entry absent; a subsequent listener-free register of the same name succeeds
and contributes exactly once). For systemPrompt (no duplicate-name check) the
two tests assert assembly is clean. Verified each fails against the pre-fix
emit-before-return-disposer form.
2026-06-15 01:06:13 +08:00
Tianyi Cui 825b57aff9 feat(llm): structured error taxonomy with a shared HarnessError base (RFC 005 pt 2)
Introduce HarnessError in dsh-llm (the leaf package): a stable machine-routable
code distinct from the message, cause chaining, name from the subclass, plus
isHarnessError. LlmError, ToolArgsError, and InvariantError now extend it.

Tool failures carry the structure end-to-end: ToolExecutionResult gains
error: { name, code } (populated from a thrown HarnessError), and the loop
forwards it onto the tool/result session event (which gained the same optional
field) for retry/sandbox plugins and replay. The loop's toError wraps non-Error
throws in a HarnessError(code: UNKNOWN, cause) instead of a bare Error.

Landed last and in isolation so it's a pure upgrade over the plain Error+code
the earlier PRs used — independently revertible. Graduates RFC 005 pt 2 ->
ADR 0015; RFC 005 now fully implemented.
2026-06-14 01:07:28 +08:00
Tianyi Cui 7b07b70750 test: address Codex review of property tests (PR 3)
- llm: generator now emits finish chunks (the finish-defaults property was
  vacuously green); add a property asserting streaming and one-shot assembly
  agree on usage and finish
- agent-loop: assert the synchronous burst batches into exactly one turn; add
  a mixed-schedule property (send/settle interleavings); recordStatus returns
  its disposer; per-run timeouts so a hang loses no seed
- session: randomize the noise/message interleaving (was a fixed alternation)
- tools: exclude non-finite doubles from generated numeric args (JSON-real)
2026-06-14 00:24:23 +08:00
Tianyi Cui 2f6d3b8539 test: property-based tests for protocol-shaped code (RFC 001)
Adds fast-check + one tests/properties.spec.ts per protocol-shaped package
(llm/BlockAssembler, session, tools/schema DSL, agent-loop scheduling). The
tools suite includes the RFC 001<->005 composition property (generated args
satisfying a spec pass validateArgs), closing the validator/InferArgs drift
risk from ADR 0011. Loop properties are deterministic (settle on agent/status,
no sleeps).

The BlockAssembler suite found a real bug on first run: a duplicate block-end
at the same index overwrote an already-flushed block, so the streamed prefix
disagreed with final blocks(). Fixed (first close wins, matching the existing
straggler rule) + regression test. Graduates RFC 001 -> ADR 0013.
2026-06-14 00:06:25 +08:00
Tianyi Cui 370b5d3aab Add assertNever with closed-vs-extensible exhaustiveness guidance
assertNever (dsh-llm) marks unreachable defaults on CLOSED unions:
adding a StreamChunk variant now breaks compilation at
BlockAssembler.push, and a value escaping its type at runtime throws
with diagnostics. The module doc and a new AGENTS.md convention spell
out the dividing line: merge-extensible unions (SessionEventMap,
ContentBlockMap, …) must NOT use assertNever — plugin-added variants
are valid unknown values there; handle known cases and fall through
with a comment.
2026-06-11 15:21:25 +08:00
Tianyi Cui 225ed051b1 Add branded ID types: CallId, SessionId, AgentId
Nominal string types via a unique-symbol brand (zero runtime cost):
an AgentId can no longer be passed where a CallId is expected. Each
core package brands the IDs it owns — CallId in dsh-llm (tool-call
correlation across blocks, chunks, session events, and execution
results), SessionId in dsh-session, AgentId in dsh-agent. Construction
goes through same-named factory functions; public string-in APIs
(sessions.create, agentLoop.create) keep accepting plain strings and
brand internally. Policy note in the brand module: brand IDs that
cross package boundaries, not every string.
2026-06-11 15:17:56 +08:00
Tianyi Cui bfb034830f Enforce 100% per-file test coverage on packages/*/src
vitest coverage (v8 provider) with per-file 100% thresholds for
statements, branches, functions, and lines. Scope: our runtime source
only — types-only files, vendor/ (upstream code), and examples/
(exercised by the demo smoke test) are excluded. yarn test:coverage
runs the gate.

59 tests added to close every gap: llm generate-waterfall and adapter
disposal; assembler edge protocol (duplicate block-start, stragglers
after block-end, id fallback, usage omission, invariant violation);
the whole Inbox surface incl. the wakeup-overwrite race; LoopAgent
disposed-state throws and double-stop idempotence; config-driven agent
creation; loop backstop catches (throwing turn-start/turn-end
listeners, non-Error throws, non-JSON tool arguments); system-prompt
dynamic sections and disposer paths; tools errorMessage fallbacks and
the full schema-DSL emission matrix. Genuinely unreachable defensive
guards carry /* v8 ignore */ comments with stated reasons rather than
deletion (132 tests total).
2026-06-11 14:58:36 +08:00
Tianyi Cui d2fb352f3e Enable maximum-strict TypeScript across our packages
tsconfig.base.json adds noUncheckedIndexedAccess,
exactOptionalPropertyTypes, noImplicitOverride,
noFallthroughCasesInSwitch, noUnusedLocals, and noUnusedParameters on
top of strict. Vendored packages opt out of the new flags locally
(their tsconfigs are ours to regenerate; their source is not), keeping
upstream-sync friendliness.

Our code fixed accordingly: index accesses acknowledge undefined
(assembler flush cursors, lastTurnNumber); optional properties are
omitted instead of set-to-undefined (GenerateResult.usage,
ToolDefinition.strict, GenerateOptions.system/tools, error payloads
via an errorData helper); Session.onAppend is explicitly
`(…) => void | undefined`; tests and examples updated for unused
parameters and indexed access.
2026-06-11 14:02:47 +08:00
Tianyi Cui d5a1d9bb75 Add abstract service interface packages
@deepseek-ai/dsh-llm: provider-neutral content-block vocabulary
(merge-extensible maps), raw StreamChunk protocol, ToolSchema,
abstract LlmAdapter, LlmService adapter registry, BlockAssembler.

@deepseek-ai/dsh-session: event-sourced Session (append-only log,
deriveMessages; context/steering render as tagged envelopes),
SessionStore, session/event + awaited session/flush durability seam.

@deepseek-ai/dsh-system-prompt: ordered sections + tool-schema
providers; assemble() through the system-prompt/assemble waterfall.
Tool schemas are part of the assembly by design.

@deepseek-ai/dsh-tools: tool registry feeding schemas into the
assembly; execute() through the tools/execute waterfall (the single
sandbox/permission/hook seam).

@deepseek-ai/dsh-agent: Agent interface (send/steer/inject/abort,
spawn/fork TODO seams), AgentRegistry, and the full agent/* event
taxonomy so plugins never depend on the concrete loop.
2026-06-11 10:54:06 +08:00