DSH mock implementation specification

August 18, 2026 · View on GitHub

Status: proposed v1. The rules in this document are normative. MUST and MUST NOT are requirements; SHOULD and SHOULD NOT are defaults that may be changed only with an explicit compatibility decision.

This plugin is an external Cordis plugin for DeepSeek Harness (DSH). Its purpose is deterministic execution of one mock command or a replay script through the real DSH agent loop and tool runtime.

The implementation lives at:

C:\Users\yifan\code\Ephemeral-AI-Lab\dsh-plugins\mock

No implementation step covered by this specification may modify the deepseek-harness source tree.

1. Fixed architecture

The plugin MUST use the public @deepseek-ai/dsh-llm LlmAdapter contract and the public Cordis plugin contract. It MUST register the provider route mock and the model id mock:

per-turn /mock command
        |
        v
mock command/replay controller
        |
        v
public DSH per-turn model selection: { provider: "mock", model: "mock" }
        |
        v
MockAdapter (public @deepseek-ai/dsh-llm LlmAdapter)
        |
        v
real DSH AgentLoop
        |
        v
real DSH ToolRuntime -> registered tool

The adapter emits model output only. It MUST NOT invoke a tool implementation, replace ctx.llm, replace AgentLoop, replace ToolRuntime, or install a second tool-validation or policy engine. In particular, it MUST NOT call a tool's execute function directly.

The plugin is a command surface, not a model-page selection feature. The DSH model page MUST NOT be required to select mock/mock, and the plugin MUST NOT permanently change an agent's configured provider or model. A command selects the mock route for that turn only. The route may be discoverable to the host's provider APIs for plumbing and tests, but it is not the user-facing control surface.

The command layer MUST use the host's public command and per-turn generation hooks. It MUST NOT call the adapter to manufacture a separate conversation or run a private agent loop. If a supported DSH version does not expose a public per-turn model override, that integration gap MUST be reported rather than solved by mutating the session model or replacing the loop.

2. Provider and plugin contract

The Cordis entry point MUST have the normal external-plugin shape, using the exact public typings of the supported DSH version:

import type { Context } from '@deepseek-ai/cordis'
import { MockAdapter } from './mock-adapter.js'

export const name = 'mock'
export const inject = ['llm']

export function apply(ctx: Context): void {
  const adapter = new MockAdapter()
  const registration = ctx.llm.registerAdapter(['mock'], adapter)
  ctx.effect(() => {
    adapter.dispose()
    return registration
  }, 'mock cleanup')
}

The exact cleanup shape may follow the installed Cordis version, but disposal MUST unregister mock, abort or release adapter-owned state, remove command/UI/event subscriptions, and remove all session plans. The package MUST depend only on public DSH/Cordis packages at runtime, with DSH packages supplied by the host as peer dependencies where appropriate.

The adapter MUST expose provider metadata for mock and model metadata for mock, for example:

providerInfo("mock") -> { id: "mock", name: "Mock" }
resolve/list model("mock", "mock")
  -> { provider: "mock", id: "mock", name: "Deterministic Mock Model" }

Metadata availability MUST NOT cause the plugin to switch the persistent model-page selection.

3. Slash-command control surface

The plugin MUST register /mock through the public DSH slash-command/message command surface. A slash command received as a normal message, followup, or steer command MUST be parsed the same way and scheduled as the next mock turn. Followup and steer do not create a special execution mode.

Every accepted mock command creates a turn-local plan keyed by the current DSH sessionId and a unique mock turn id. The command dispatcher MUST hand that plan to the normal AgentLoop through the public per-turn route override:

{ provider: "mock", model: "mock", mockTurnId: "..." }

No global mockEnabled flag may affect later turns. After a mock turn is complete, a non-slash message MUST use the real provider configured for that turn. Mock and real-provider turns MAY coexist in one session transcript and MUST remain distinguishable by their per-turn route and durable events.

3.1 /mock run

/mock run MUST accept exactly one executable unit: either one tool call or one parallel group. Tool arguments MUST be JSON objects. The command grammar is:

/mock run tool_name(JSON_OBJECT)
/mock run [tool_name(JSON_OBJECT) tool_name(JSON_OBJECT) ...]

Examples:

/mock run probe_tool({"value":"ok"})
/mock run exec_command({"cmd":"Get-Location"})
/mock run [probe_a({"value":"a"}) probe_b({})]

The bracket form MUST contain one or more complete tool-call expressions. The command parser MUST reject JavaScript syntax, single-quoted or unquoted object literals, comments, trailing statements, multiple sequential calls, and malformed JSON.

/mock run MUST NOT accept wait syntax. In particular, --wait, wait, a wait object, a semicolon-separated second command, or a second top-level call MUST produce a command error and no tool call. A wait can be authored only in a canonical replay script.

The command parser validates command shape and JSON syntax only. Unknown-tool and tool-schema validation MUST be delegated to the real DSH tool runtime.

3.2 /mock replay

The replay grammar is:

/mock replay <path>
/mock replay <path> --overwrite-wait-time-ms <N>

There MUST be exactly one path and, if present, one non-negative integer N. The input adapter is selected from the file content or a documented format detector. v1 MUST support canonical mock-script JSON and DSH JSONL. Additional formats MAY be registered later, but each adapter MUST output canonical JSON before validation and execution.

--overwrite-wait-time-ms N is a timing override, not a file overwrite flag. It MUST replace explicit wait values in the in-memory canonical plan only. It MUST NOT replace, modify, rename, copy, or delete the input file.

4. Canonical mock-script format

All command and replay inputs MUST normalize to this versioned JSON shape:

{
  "type": "dsh-mock-script",
  "version": 1,
  "steps": [
    { "tool": "probe_tool", "args": { "value": "ok" } },
    {
      "parallel": [
        { "tool": "probe_a", "args": {} },
        { "tool": "probe_b", "args": { "value": 2 } }
      ]
    },
    { "wait": 250 },
    { "tool": "probe_c", "args": {} }
  ]
}

The canonical grammar is:

Script       = { type: "dsh-mock-script", version: 1, steps: Step[] }
ToolStep     = { tool: ToolName, args: JsonObject }
ParallelStep = { parallel: NonEmptyArray<ToolStep> }
WaitStep     = { wait: NonNegativeInteger }
Step         = ToolStep | ParallelStep | WaitStep

Unknown top-level or step properties MUST be rejected. Tool names MUST obey the public DSH tool-name rules. JSON arguments MUST remain data; the validator MUST never evaluate them as code.

WaitStep is a scheduling step, not a tool call. A wait MUST occur only strictly between two top-level executable steps. It MUST NOT be the first or last step, MUST NOT be consecutive with another wait, and MUST NOT occur inside a parallel array. A parallel group contains tool steps only. Every tool inside one group is one sibling response from the adapter and is eligible for the normal DSH parallel tool execution behavior.

The following script has three executable steps, not five:

{
  "type": "dsh-mock-script",
  "version": 1,
  "steps": [
    { "tool": "a", "args": {} },
    { "wait": 1000 },
    { "parallel": [
      { "tool": "b", "args": {} },
      { "tool": "c", "args": {} }
    ] },
    { "wait": 500 },
    { "tool": "d", "args": {} }
  ]
}

The unified canonical validator MUST be the same validator for self-authored JSON and every converted input format. Format adapters MUST not duplicate canonical validation rules.

5. Timing semantics

Execution is ordered by top-level executable steps. A parallel group is one step; its members have no plugin-owned inter-member wait. A wait belongs to the edge between the preceding and following top-level executable steps.

When no explicit wait exists on an edge, the runner MUST use a default gap of 100 ms. An explicit wait MUST be preserved by default. When --overwrite-wait-time-ms N is supplied, it MUST replace explicit wait values only; edges that had no explicit wait MUST continue to use the 100 ms default. Thus, the option does not turn every gap into N.

The effective schedule is:

E1 -- explicit wait, or 100 ms if absent --> E2
E2 -- explicit wait, or 100 ms if absent --> E3

The runner MUST implement waits as cancellation-aware delays in the adapter's next-response path after the preceding tool result(s) have arrived. It MUST not execute a local copy of the tool loop. A wait MUST not emit a tool-call chunk, a fake tool result, or a progress step. A cancelled wait MUST settle by the normal DSH cancellation path.

If a converter derives timing from source timestamps, it MUST convert only timestamps with known comparable units and a reliable top-level step boundary. The resulting non-negative integer gap MUST be represented as an explicit wait step. Missing, incomparable, or ambiguous timestamps MUST not be turned into guessed waits; normal 100 ms defaults apply instead. Timestamp preservation is playback pacing and is not a promise to reproduce historical tool durations.

6. Input adapters and DSH JSONL conversion

An input adapter has one responsibility: parse its source and return canonical dsh-mock-script JSON plus source locations for diagnostics. It MUST NOT execute tools, emit model chunks, or bypass the unified validator.

The DSH JSONL adapter MUST implement these rules:

  1. Tool-call blocks that are siblings in one assistant/message are one parallel step. Their order in the array MUST be retained within the group.
  2. Distinct DSH agent steps MUST become distinct sequential top-level steps, ordered by the source step sequence or equivalent durable ordering.
  3. Interleaved tool/call and tool/result records MUST not convert siblings from one assistant step into sequential steps.
  4. Call name and JSON arguments MUST be copied into tool and args.
  5. Historical tool results, tool-result content, approval outcomes, and prior tool output MUST NOT be replayed as input or synthesized as new results. Replay executes fresh calls through the current DSH runtime.
  6. Call ids MAY be used for source diagnostics but MUST NOT be reused as live call ids; the adapter generates new ids when it emits model output.
  7. When reliable comparable step timestamps exist, the adapter MAY insert explicit waits according to section 5. It MUST report a conversion error instead of silently choosing between conflicting timestamp boundaries.

The adapter MUST report a CONVERSION_MISMATCH error when records that claim to describe the same assistant step disagree about tool name, call identity, or arguments. It MUST include the input path (when known), line/record locations, and the conflicting fields. It MUST NOT silently prefer one record kind merely because it is easier to parse. A malformed JSONL record, incomplete tool-call, non-object arguments, invalid tool name, unsupported script version, or illegal wait placement is an invalid script/conversion error before execution.

Other input format adapters MUST obey the same contract. Their output MUST be canonical JSON and their mismatch diagnostics MUST be surfaced rather than hidden behind a generic parse failure.

7. Replay execution inside the real AgentLoop

The adapter is a deterministic model, not a replay executor. For each executable plan step, its stream() method MUST emit ordinary DSH assistant tool-call chunks. The AgentLoop then persists the assistant message and uses the real ToolRuntime to resolve, validate, authorize, cancel, execute, render, and persist each tool result.

For a single tool step, the adapter emits one call. For a parallel step, it emits one assistant response containing one complete tool-call block per member and finishes with tool-calls. It MUST then wait for the corresponding current turn tool result message(s) before emitting the next scripted model response. The adapter MUST never inspect or manufacture a result by itself.

The sequence is:

stream(E1) -> complete tool-call block(s) -> finish: tool-calls
AgentLoop -> normal ToolRuntime -> durable tool/result event(s)
stream(wait edge, if any) -> next complete tool-call block(s)
AgentLoop -> normal ToolRuntime -> durable tool/result event(s)
stream(final response) -> one full text delta -> finish: stop

Only the final response ends the mock turn. Intermediate responses MUST end with finish: { kind: "tool-calls" }; they MUST NOT end with stop merely because one scripted step completed. Waits do not end a turn.

For v1 the adapter MUST emit complete tool-call blocks. It MUST NOT emit tool-call deltas. A tool call is represented by a block-start followed by a block-end containing the complete tool-call block and JSON-stringified arguments. The final response MUST contain one complete text delta (and may have the corresponding block start/end) followed by finish: { kind: "stop" }. There is no requirement to emulate token-by-token text or tool-call deltas.

On the final successful response, the text SHOULD be a short deterministic summary. The DSH tool-call and tool-result events remain authoritative; the summary is presentation only. The adapter MUST honor the request AbortSignal during emission and waits.

7.1 Fail-fast behavior

The adapter MUST preserve the real runtime's unknown-tool and invalid-argument errors. It MUST not pre-approve or locally validate them in a way that changes DSH behavior. When the current result reports UNKNOWN_TOOL or INVALID_ARGS, the mock plan MUST enter a terminal failed state and no later top-level step may be emitted. The same terminal stop SHOULD apply to policy/approval denial, tool execution failure, invalid tool output, and other isError results so a failed deterministic replay cannot silently diverge.

For a parallel group, all sibling calls already emitted in that response may finish through the normal DSH runtime. A failure in one member MUST prevent later top-level steps, but it cannot retroactively unsend or cancel sibling calls unless normal DSH cancellation does so.

Background-job tools are allowed. They MUST run through the normal DSH tool runtime, and a later scripted write_stdin-style call may interact with the real job state. The plugin does not copy, virtualize, or simulate that state. Nested or subagent tools are unsupported for deterministic replay. The replay preflight SHOULD reject a tool marked as nested/subagent by public tool metadata with an explicit unsupported-tool error. The plugin MUST NOT claim a deterministic replay guarantee when the public runtime cannot classify such a tool.

8. Lifecycle, isolation, events, and errors

8.1 Agent status

The plugin MUST use normal DSH agent status transitions. On acceptance of a mock command, the agent becomes running when the normal turn starts. It remains running across tool execution, waits, and intermediate scripted responses. It becomes idle only after the final response, a normal terminal error, cancellation, or disposal has settled. The plugin MUST NOT publish a fake idle status between script steps.

8.2 Session isolation

All mutable plan state MUST be keyed by the DSH sessionId and a mock turn id. A global pending-call or global current-script variable is forbidden. Two sessions may replay different scripts concurrently and must not consume each other's calls, results, waits, status, events, or cleanup. A missing sessionId MUST be rejected clearly or handled as request-local state that is destroyed before the request returns; it MUST never use a shared fallback key.

The adapter MUST associate a result with the expected call id(s) and current plan step. A stale, duplicate, or cross-session result MUST produce a diagnostic terminal error and MUST NOT advance the plan.

8.3 Cleanup

Plan state, abort listeners, wait timers, command subscriptions, UI subscriptions, and event subscriptions MUST be released on final success, terminal error, cancellation, session teardown, and plugin disposal. Cleanup MUST be idempotent. A disposed plugin MUST not emit another model response or retain references to an agent/session.

8.4 Durable events

The normal DSH durable events are authoritative and MUST remain intact:

  • assistant messages containing the emitted tool-call blocks;
  • real tool/call events;
  • real tool/result events, including runtime error codes and output;
  • the final assistant message.

The plugin SHOULD persist command/replay lifecycle events through the public DSH session event/persistence API, with at least the session id, mock turn id, source mode (run or replay), source format/path when allowed, script id, executable-step count, and terminal status. Step events MAY include started, completed, failed, waiting, or cancelled, but waits MUST NOT be represented as tool events. Plugin-owned events MUST be serializable and must not duplicate or replace DSH tool cards. If a supported DSH release does not expose a public durable-event extension point, the plugin MUST retain the normal DSH events and report that optional mock lifecycle persistence is unavailable; it MUST NOT write a private database or edit DSH source.

Error classes presented to callers/UI MUST remain distinguishable:

ClassMeaningExecution effect
INVALID_COMMANDSlash grammar or JSON syntax is invalidNo tool call; no plan starts
INVALID_SCRIPTCanonical shape, wait placement, version, or adapter output is invalidNo tool call; no plan starts
CONVERSION_MISMATCHSource records disagreeNo tool call; no plan starts
UNKNOWN_TOOLReal DSH registry cannot resolve a requested toolReal tool result; stop later steps
INVALID_ARGSReal DSH schema rejects argumentsReal tool result; stop later steps
UNSUPPORTED_NESTED_TOOLReplay cannot guarantee nested/subagent determinismNo such replay step; stop
runtime/policy/output errorNormal DSH tool failure or denialPreserve normal event/error; stop the mock plan
cancellationNormal DSH cancellationSettle stream, clean up, become idle

9. UI/UX contract

If the plugin contributes UI, it MUST provide a compact generic active indicator above the composer while the current mock turn is running. It MUST not say Running 2 tools in parallel or otherwise make a parallel group a second tool-progress surface.

The UI MUST provide:

  1. An invalid-script card for invalid command syntax, invalid canonical JSON, conversion errors, and conversion mismatches. It MUST show enough source location/error detail to fix the input and MUST show that no tool ran.
  2. An invalid-tool stop card for an unknown tool or invalid arguments. It MUST show the failing tool and runtime error code, state that later steps were stopped, and leave the actual DSH tool card visible.
  3. Script progress where the denominator counts executable top-level steps only. Wait steps are excluded. A parallel group counts as one step.

Actual DSH tool-call and tool-result cards are authoritative for member-level progress, approvals, errors, and output. Plugin UI MUST not synthesize a competing tool card or rewrite a result.

The detailed UI/UX placement, state model, progress rules, cancellation behavior, and rendering logic are defined in ui/ui-ux.md.

10. File and replay safety policy

Replay MUST read the requested source and convert/validate it in memory. It MUST execute the resulting canonical object directly. It MUST NOT make a physical copy of the DSH JSONL, write a converted canonical script, create a temporary replay file, or overwrite the source. Format adapters MUST expose canonical JSON as their result so an explicit future conversion/export operation can persist it; replay itself MUST remain in-memory.

The --overwrite-wait-time-ms option affects only explicit wait values in the in-memory plan. It MUST never be interpreted as permission to overwrite any file.

One policy decision remains intentionally open and MUST be resolved before shipping path-based replay: how <path> is resolved and constrained. The implementation owner MUST choose and document a host-compatible policy for relative paths, absolute paths, current-session working directory, allowed roots, symlinks, and path traversal. This task does not silently choose session-cwd, plugin-directory, or unrestricted absolute-path behavior. Until that decision is made, the reader MUST use the host's existing approved read-only file-access policy or reject path replay with a clear error. In all policies, source and destination overwrite behavior remains disabled unless a future specification explicitly adds an output destination, overwrite rule, and atomicity/permission behavior.

11. Implementation folder structure

The package should remain small and external. Generated lib/ output is a build artifact and is not hand-authored. For v1, command routing, replay state, timing, and the compact UI status model MUST remain in the existing source modules unless a separate module is proven necessary. The required source set is index.ts, mock-adapter.ts, parser.ts, and converter.ts; there MUST be no scenario.ts.

mock/
├── src/
│   ├── index.ts                 # Cordis entry point and public registration
│   ├── commands.ts              # /mock run and /mock replay parsing/routing
│   ├── mock-adapter.ts          # public LlmAdapter and scripted responses
│   ├── parser.ts                # strict command/JSON parsing primitives
│   ├── script.ts                # canonical types, validator, timing plan
│   ├── replay.ts                # in-memory replay orchestration and cleanup
│   ├── converter.ts             # input-adapter registry and DSH JSONL adapter
│   ├── events.ts                # public durable mock lifecycle events
│   └── ui.ts                    # indicator, invalid cards, and progress model
├── test/
│   ├── commands.test.ts         # slash grammar and per-turn routing
│   ├── parser.test.ts            # JSON-only parser cases
│   ├── script.test.ts            # canonical validation and wait rules
│   ├── converter.test.ts         # DSH JSONL and mismatch diagnostics
│   ├── adapter.test.ts           # complete chunk stream and session isolation
│   ├── replay.test.ts            # timing, fail-fast, no-write behavior
│   ├── integration.test.ts       # real AgentLoop and real ToolRuntime
│   ├── plugin.test.ts            # compiled external load and disposal
│   └── ui.test.ts                # cards, indicator, and step counting
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── cordis.patch.yml
├── README.md
└── spec.md

The normative v1 source set is only:

src/index.ts
src/mock-adapter.ts
src/parser.ts
src/converter.ts
test/parser.test.ts
test/converter.test.ts
test/adapter.test.ts
test/integration.test.ts
test/plugin.test.ts
test/ui.test.ts

The additional names in the illustrative tree are optional extraction points, not required files. They MUST NOT be added merely to satisfy this document.

The required UI design document is separate from the v1 TypeScript source:

ui/
└── ui-ux.md

The ui/ folder is the design and future host-integration boundary. It does not authorize edits to the DeepSeek Harness web client; the plugin only emits the state/events needed by the host integration.

No scenario.ts, private DSH imports, replacement runtime, private database, or source file under deepseek-harness is part of this structure.

12. Verification requirements

The package MUST pass its typecheck, build, and test commands using the public DSH package versions declared by the package. Tests MUST verify:

Command and canonical validation

  • single /mock run accepts an object and rejects malformed/non-JSON args;
  • one bracketed /mock run [tool(... ) tool(...)] group is accepted;
  • waits and multiple sequential calls are rejected by /mock run;
  • canonical single, parallel, and wait steps validate;
  • leading, trailing, consecutive, and nested waits fail;
  • absent waits receive 100 ms, explicit waits survive unchanged, and the overwrite option replaces explicit waits only;
  • unsupported versions, unknown fields, invalid JSON objects, and conversion mismatches produce invalid-script diagnostics.

Adapter and real-loop contract

  • valid input emits complete tool-call blocks and finish: tool-calls;
  • no tool-call deltas are emitted;
  • the final response emits one full text delta and finish: stop;
  • the adapter does not consult a tool registry or directly execute a tool;
  • results are awaited before the next scripted response;
  • unknown tools and invalid arguments come from the real ToolRuntime and stop later steps;
  • policy/approval, execution errors, invalid output, and cancellation preserve normal DSH behavior;
  • background-job tools remain usable, while nested/subagent replay is rejected or explicitly reported unsupported;
  • concurrent sessions remain isolated and cleanup releases all state.

DSH JSONL conversion

  • sibling calls in one assistant/message become one parallel group;
  • calls from different agent steps stay sequential;
  • timestamp gaps are converted only when reliable and are represented as explicit waits;
  • historical results are omitted;
  • conflicting records produce CONVERSION_MISMATCH with source locations;
  • every adapter output passes the same canonical validator.

External/plugin/UI behavior

  • the compiled package loads through a normal Cordis composition;
  • mock and mock resolve, then disappear on plugin disposal;
  • the persistent model-page selection is unchanged;
  • slash mock, followup/steer mock, and real-provider turns coexist in one session;
  • agent status remains running across intermediate steps and waits and becomes idle only at terminal completion/cancellation/error;
  • durable DSH tool events remain authoritative;
  • invalid-script, invalid-tool-stop, generic active indicator, and executable step-count behavior match section 9;
  • replay performs no physical copy or write.

The tests MUST use a small public-API DSH composition with in-memory tools for the core suite. Any shell/background-job smoke test MUST be opt-in and clearly environment-dependent. The test suite MUST not require modifying or importing private implementation files from the DeepSeek Harness checkout.

13. Limitations and non-goals

Version 1 does not replace any DSH runtime, does not make a real model request, does not expose model-page mock selection, does not accept waits in /mock run, does not replay historical tool results, and does not guarantee identical wall-clock behavior when real tools have different durations. It supports one tool or one parallel group for /mock run; multi-step behavior belongs to canonical replay. It supports background-job tools through the real runtime, but nested/subagent tools are outside deterministic replay. It does not persist converted output or make a path policy decision that has not been approved.