Merge remote-tracking branch 'origin/master' into worktree/provider-routed-llm-adapters

# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/sandbox-acp-agent/cordis.yml
#	examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl
#	packages/compact/compact-basic/README.md
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/tests/properties.spec.ts
#	packages/core/session/README.md
#	packages/core/session/src/types.ts
#	packages/core/session/tests/derived-cache.spec.ts
#	packages/llm/llm-deepseek/src/index.ts
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm-pi-ai/src/convert.ts
#	packages/llm/llm-pi-ai/tests/adapter.spec.ts
#	packages/llm/llm/README.md
#	packages/llm/llm/src/call-config.ts
#	packages/llm/llm/src/index.ts
#	packages/ui/acp-agent/src/index.ts
#	packages/ui/acp/tests/harness.ts
#	packages/ui/jsonrpc/README.md
#	packages/ui/jsonrpc/src/server.ts
#	packages/ui/stdio-agent/README.md
#	packages/ui/stdio-agent/src/index.ts
#	python/sdk/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-07-14 22:17:50 +08:00
672 changed files with 10295 additions and 14200 deletions

View File

@@ -1,8 +1,8 @@
# dsh-invariants
Dev-mode event-contract assertions. This pure-listener plugin checks relationships among session events, agent states, scoped dispatches, and model requests at runtime; it does not own or change product behavior.
Runtime event-contract assertions intended for development diagnostics. This pure-listener plugin checks relationships among session events, agent states, scoped dispatches, and model requests; it does not own or change product behavior.
**Off in production.** Enable it in tests and the demos, where a contract violation should fail loudly. It costs nothing when not registered, and doubles as executable documentation of the event taxonomy — the assertions *are* the contract.
The plugin has no environment guard: it is active wherever it is registered. The default [`dsh-agent-core`](../../core/agent-core/README.md) bundle mounts it unconditionally; a custom composition can omit it when the runtime cost is undesirable. It doubles as executable documentation of the event taxonomy — the assertions *are* the contract.
Session itself owns immutable log storage in every composition: it takes one lossless JSON snapshot of each accepted event, deep-freezes that record, and exposes the log through immutable array snapshots. The invariants plugin checks the cross-record and cross-seam rules that storage immutability cannot express.
@@ -39,14 +39,23 @@ Agent status (per agent):
Model requests (on `llm/stream`):
- **a loop-built request is exactly what the log reconstructs** — a frozen request with a live `sessionId` (the loop-built marker; hand-built one-shots like compaction's summarize are unfrozen and skipped) must carry frozen `messages` deep-equal to the derivation over the log prefix strictly before the in-flight step's `step/start` (rebuilt through a FRESH `Session`, so the live cache cannot vouch for itself — and boundary-correct: content logged after `step/start` legitimately belongs to the next request), and every non-content field must equal the fold of the log's `request/header*` events (see [the reconstructability RFC](../../../docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). Registered with `prepend: true` so a short-circuiting `llm/stream` listener (the replay adapter) cannot silence it; prepend orders it against append-registered listeners only — correctness rests on the seq-bounded rebuild, never listener timing.
- **a loop-built request is exactly what the log reconstructs** — a frozen request with a live `sessionId` is rebuilt through a fresh `Session` from the prefix before its in-flight `step/start`; later content belongs to the next request, and hand-built unfrozen one-shots are excluded. Frozen messages must match that derivation, while every other field matches folded `request/header*` events. The prepended check runs before ordinary short-circuiting stream listeners, but correctness comes from the sequence boundary rather than listener timing. See the [reconstructability RFC](../../../docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md).
On any violation it throws `InvariantError` (`code: 'INVARIANT'`).
## Why runtime assertions remain useful
Session enforces the per-record storage boundary at runtime, where a cast cannot bypass it. Pervasive `DeepReadonly<SessionEvent>` types would add noise across consumers without expressing relationships such as turn/step nesting, subject-correct scoped dispatch, or equality between a request and its log reconstruction. This plugin checks those relationships in development while `dsh-session` keeps history immutable in every composition. See [source-owned session immutability and dev-mode invariants](../../../docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md).
Session enforces the per-record storage boundary at runtime, where a cast cannot bypass it. Pervasive `DeepReadonly<SessionEvent>` types would add noise across consumers without expressing relationships such as turn/step nesting, subject-correct scoped dispatch, or equality between a request and its log reconstruction. This plugin checks those relationships wherever it is mounted while `dsh-session` keeps history immutable in every composition. See [source-owned session immutability and dev-mode invariants](../../../docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md).
## Seeded sessions
A seeded or forked session arrives with events already in its log because construction does not emit `session/event` for each seed record. `Session` validates, snapshots, and freezes every seed record before accepting it; on `session/created`, this plugin replays the accepted log only to rebuild and check its relational trace state.
## Model Experience
None, as this observer only validates events and frozen requests and never rewrites prompts, schemas, messages, or streams.
## Known Limitations and Deferred Work
- **The request-reconstructability assertion covers loop-built requests only** — hand-built one-shots (e.g. compaction's summarize call) carry no live `sessionId` marker and are skipped.
- **Merge-extended event families get no family-specific assertions** — `compact/*` lock pairing and `hook/*` invoked/result pairing are not checked here; only the core turn/step/chunk/tool-result contract is.

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-invariants",
"description": "Dev-mode event-contract assertions for the DeepSeek Harness",
"description": "Runtime event-contract assertions for DeepSeek Harness development diagnostics",
"version": "0.0.1",
"private": true,
"type": "module",

View File

@@ -1,19 +1,9 @@
/**
* Dev-mode invariants: a pure-listener plugin that asserts relationships in
* the harness event contract at runtime.
*
* Everything is a plugin — this is just listeners on `session/created`,
* `session/event`, `agent/status`, and the scoped dispatch and request seams.
* It is **off in production**: enable it in tests and demos, where a contract
* violation should be a loud failure rather than a subtle one. It doubles as
* executable documentation of the event taxonomy: the assertions below are
* the contract.
*
* Session owns immutable log storage: it snapshots and deep-freezes every
* accepted event at the source. This plugin checks relationships that one
* event's types and immutability cannot express, including turn/step nesting,
* scoped dispatch, status transitions, and request reconstructability.
*
* Runtime listeners that fail loudly when cross-event contracts are broken:
* turn and step nesting, scoped dispatch, status transitions, and request
* reconstruction. The plugin has no environment guard and is active wherever
* mounted, including the default `dsh-agent-core` bundle; custom compositions
* may omit it. Sessions still own event snapshots and freezing.
* @module @deepseek-ai/dsh-invariants
*/
@@ -328,23 +318,19 @@ function replayEvent(trace: SessionTrace, event: SessionEvent): void {
applyTransition(trace, validateEvent(trace, event))
}
/** Legal agent status transitions (the only state machine the loop guarantees). */
/** Allow an initial observation, idle/running transitions, and terminal disposal; reject repeats and leaving disposed. */
function checkTransition(from: AgentStatus | undefined, to: AgentStatus): void {
// First observation: any status is a valid starting point.
if (from === undefined) return
// A no-op transition is illegal — setStatus dedups, so we never see it.
if (from === to) {
throw new InvariantError(`agent/status repeated ${to} (no-op transition)`)
}
// Leaving `disposed` is illegal — disposal is terminal.
if (from === 'disposed') {
throw new InvariantError(`agent/status left terminal state disposed → ${to}`)
}
// idle↔running and (idle|running)→disposed are all legal; nothing else exists.
}
/**
* Register the dev-mode invariants. Contributions are effect-scoped, so
* Register the runtime invariants. Contributions are effect-scoped, so
* disposing the plugin fiber removes all listeners (HMR-safe). On (re-)apply
* the trace state is rebuilt by replaying each existing session's log, so a
* hot reload mid-turn does not falsely reject the next event.

View File

@@ -325,19 +325,17 @@ describe('HMR state rebuild', () => {
it('rebuilds trace state for a session that exists at (re-)apply time', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
// First registration, mid-turn: a turn is open when the plugin reloads.
const first = await ctx.plugin(Invariants)
const session = ctx.sessions.create()
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('step/start', { turn: 1, step: 1 })
await first.dispose()
// Re-apply (HMR): the fresh fiber must replay the existing log so the open
// step is known — the next chunk must NOT be a false positive.
// Re-apply mid-step: the new fiber must reconstruct the open boundaries from the log.
await ctx.plugin(Invariants)
expect(() => session.append('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'h' } }))
.not.toThrow()
// And a genuine violation is still caught after the rebuild.
// Rebuild must not disable later violations.
expect(() => session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }))
.toThrow(/turn 1 is still open/)
})
@@ -541,25 +539,17 @@ describe('surface invariants', () => {
})
it('rejects sourceEventSeqs referencing unknown seq (gap in event log)', async () => {
// The unknown-seq check fires when a ref passes the "earlier" test but is
// not in knownSeqs — only possible with a gap in seqs. We create a gap by
// directly manipulating the private log array to skip a seq.
// Create an impossible-through-public-API gap so seq 2 is earlier but unknown.
const { ctx } = await setup()
const session = ctx.sessions.create()
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('step/start', { turn: 1, step: 1 })
// Push a fake event at seq 3 into the internal log, creating a gap at seq 2.
// The invariants plugin replays session.events on every append, so it sees
// this gap during trace reconstruction.
;(session as unknown as { log: unknown[] }).log.push({
type: 'assistant/chunk',
seq: 3,
time: Date.now(),
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'x' } },
})
// Now the log has seqs 0, 1, 3 (gap at 2). Append at what session believes
// is seq 3 (log.length). Reference seq 2: passes earlier (2 < 3) but not
// in knownSeqs ({0, 1, 3} — gap at 2).
expect(() => {
session.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [] }, { surfaceOp: 'append', sourceEventSeqs: [2] })
}).toThrow(/unknown seq 2/)
@@ -803,12 +793,8 @@ describe('request-reconstruction cross-check (llm/stream)', () => {
describe('request cross-check ordering (prepend)', () => {
it('runs ahead of a short-circuiting llm/stream listener registered before it', async () => {
// The replay adapter returns its chunks WITHOUT calling next(), which
// would silence a later-registered check — snapshot compositions load
// replay before the app bundle that loads invariants. The check prepends,
// so it fires ahead of append-registered listeners regardless of load
// order. (Prepend orders it against APPENDED listeners only; correctness
// rests on the seq-bounded rebuild, not on listener timing.)
// Replay short-circuits without next(), so the check prepends ahead of ordinary listeners;
// correctness still comes from its sequence-bounded rebuild, not listener timing.
const ctx = new Context()
await ctx.plugin(SessionStore)
ctx.on('llm/stream', () => (async function* () {})() as never) // short-circuits, no next()