Rename RFCs to Agent Notes
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Agent Note: Pre-tool input rewrite — a consistent design
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The [interception-seams Agent Note](../../implemented/feature/2026-06-30-interception-seams.md) defines `tools/pre-execute` as an allow/deny/ask gate over an execution whose identity is already protected and whose arguments are deeply frozen. Claude Code's `PreToolUse` hook also offers `updatedInput`, so a faithful bridge needs an explicit rewrite mechanism. A rewrite cannot be a mutation escape hatch on the existing execution object: it must keep the durable history, audit record, presentation, and executed value consistent.
|
||||
|
||||
## The problem: three readers of pre-execution arguments
|
||||
|
||||
In the loop, a tool call's arguments are committed to the log and read by live consumers BEFORE the tool executes:
|
||||
|
||||
1. **`assistant/message`** is appended before tool dispatch — it is the model-history source `deriveMessages()` replays, so it carries the tool-call arguments the model itself emitted.
|
||||
2. **`tool/call`** is the durable AUDIT record, appended before `ctx.tools.execute()`.
|
||||
3. **Live presentation reads `tool/call.arguments`**: the ACP bridge remembers them and passes them to `presentResult`; `dsh-tool-bash` derives the card title, the rawInput, the cwd, and the terminal-vs-background treatment from them.
|
||||
|
||||
An execution-only rewrite would make the UI show one command while another ran and render the result against the wrong arguments. The registry prevents that failure mode today: it structured-clones and deep-freezes `arguments`, makes the execution identity properties non-writable, and exposes no test shim or listener path that can replace them. The rewrite design must preserve that protected-identity boundary rather than weaken it.
|
||||
|
||||
## Proposal
|
||||
|
||||
A rewrite is a pre-identity consistency transaction. When a hook supplies `updatedInput`, the effective value must be chosen before the registry constructs its immutable `ToolExecution`, and it must be reflected in all three readers atomically:
|
||||
|
||||
- The `tool/call` audit event records the REWRITTEN arguments (with the original retained in a sidecar field for the audit trail — a hook changed the call, and both the original and the effective arguments are facts worth keeping).
|
||||
- The `assistant/message` in derived history must agree with what executed — options to evaluate: rewrite the assistant message's tool-call block in place (changes what the model "sees it said"), or record a separate correction the next request carries. The CC model is that the model sees the rewrite took effect.
|
||||
- Presentation (`presentCall`/`presentResult`) reads the rewritten arguments, so the UI shows what actually ran.
|
||||
|
||||
Extending `PreToolDecision` at its current firing point is insufficient: both durable records already exist by then, and the execution identity is protected. The implementation must either move the relevant decision before the log commit or add a dedicated earlier rewrite decision over the pending model call. After the loop commits the effective arguments to history and audit, it constructs the ordinary immutable execution and runs the existing allow/deny/ask and tool pipeline unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Why not mutate the execution object?
|
||||
|
||||
Allowing a pre-execute listener to assign `exec.arguments` would provide only an execution rewrite, leaving model history, audit, and presentation unchanged. Keeping the identity protected makes such partial behavior unrepresentable. Until the consistency transaction exists, a CC/Codex bridge logs and warns about `updatedInput` rather than claiming it was honored; `TODO(pre-tool-input-rewrite)` at the loop dispatch site anchors the missing earlier phase.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A requested rewrite is resolved before `ToolExecution` identity is created and reflected in all three readers atomically: the `tool/call` audit records the rewritten arguments (the original retained in a sidecar field), derived history agrees with what executed, and presentation renders the rewritten arguments.
|
||||
- The effective `ToolExecution.arguments` remains deeply frozen and non-writable throughout pre-policy, guards, dispatch, post-policy, and final observation; no mutation shim is introduced.
|
||||
- The CC/Codex bridges honor `updatedInput` instead of logging the faithful-but-degraded warning.
|
||||
|
||||
## Risks
|
||||
|
||||
- Rewriting the `assistant/message` tool-call block changes what the model "sees it said"; whether any provider rejects that on replay is the open question that must be settled empirically before the decision shape freezes.
|
||||
- An earlier rewrite phase changes the ordering relationship among `assistant/message`, `tool/call`, hook audit events, and execution; the design must pin that ordering without weakening turn enclosure or call/result adjacency.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Does rewriting the `assistant/message` tool-call block corrupt any provider's expectation on replay, or is a separate correction safer?
|
||||
- Should the original arguments be preserved on the `tool/call` event (audit) and, if so, under what field?
|
||||
- Does the rewrite decision move before the log commit or become a dedicated earlier seam, and how do existing pre-tool allow/deny hooks avoid running twice?
|
||||
- How does this interact with a future permission `ask` flow (a user approving a rewritten call)?
|
||||
@@ -0,0 +1,108 @@
|
||||
# Agent Note: Recallable compaction — index checkpoints, a state checkpoint, and in-session history recall
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
Compaction is a one-way door. The summary the model sees carries no reference to what it shadows — the `shadowedRange` provenance lives only on the log-only `compact/summary` event — and no tool lets the model read a shadowed span back. Whatever the summarizer drops is gone from the model's reachable world, even though the append-only log holds every byte. Repeated compaction compounds this: the head checkpoint is rewritten every pass, so the request prefix takes a full prompt-cache miss each time, and earlier summaries are re-summarized generation after generation.
|
||||
|
||||
The root cause is one artifact playing two conflicting roles. An **index** wants to be frozen, chronological, and cheap; the model's **working memory** wants a global view, re-prioritization, and mutability. A single summary can be neither well.
|
||||
|
||||
No mainstream coding harness gives the model in-loop recall, and none of the surveyed implementations makes compaction prefix-cache-aware. An event-sourced session — originals durable, seq-addressable, replay-exact — is the natural substrate for both.
|
||||
|
||||
## Proposal
|
||||
|
||||
Split the checkpoint into two classes and make shadowed history reachable.
|
||||
|
||||
### Frozen index checkpoints
|
||||
|
||||
Newly stale history splits into chunks by deterministic policy: accumulate toward `chunkTokens`, snap edges with `toolPairingBalancedBefore` / `toolPairingBalancedAfter`, prefer turn boundaries, and place the final boundary as close to the retain boundary as balance allows, so the trailing slice shrinks to roughly one turn. Each chunk is compacted by one `compactRegion` call into an **index stub** (`stubTokens`, ~100–200 tokens):
|
||||
|
||||
- two or three lines of what happened;
|
||||
- a keyword line of low-frequency literal anchors — exact error strings, values, config keys — grouped by kind;
|
||||
- a code-composed footer: `[checkpoint c<summarySeq>: shadows conversation span #<start>–#<end>; originals retrievable via history_read]`. Pointers are assembled from provenance, never model-authored.
|
||||
|
||||
A committed stub is never rewritten and never re-enters a later compaction region. A stub call's input is layered: the fixed preamble and the byte-identical pass-start state checkpoint (the shared prefix across all calls in the phase), then the keyword lines of all previously committed stubs — so a new entry indexes what is distinctive to its chunk instead of repeating the directory — the one or two most recent committed stubs for chronological continuity, and the slice itself. Sibling stubs from the same pass are not inputs (the concurrent phase forbids it; turn-aligned boundaries carry local continuity instead), and the state checkpoint is background only, never material to summarize into the stub. A slice consisting of recalled content is stubbed by code alone — a pointer line, no LLM call. A failed stub call degrades the same way: its slice gets a code-only pointer stub and the pass continues, making the state rewrite the only hard LLM dependency in a pass.
|
||||
|
||||
### The state checkpoint
|
||||
|
||||
One mutable working-memory document (at most one; zero before the first pass), positioned after all stubs and before the retained tail. Each pass rewrites it from the previous state plus this pass's staled content — O(previous + new), under the merge-don't-restate rule already in the summarization prompt — covering decisions, current state, constraints, and next steps. It carries its own footer and a size cap at the scale of today's summary.
|
||||
|
||||
An inflation guard bounds the whole pass: if the post-compaction size is not strictly below the pre-compaction size, nothing commits and the turn proceeds; the attempt defers until more stale history accumulates. The guard compares one metric on both sides — provider-reported usage from the request path, falling back to the character estimator on both sides.
|
||||
|
||||
### Pass execution
|
||||
|
||||
- Chunk slices are surface position ranges. A pass runs two phases: all summarize calls execute concurrently, buffered off-surface; then regions commit strictly left to right — chunks first, trailing slice last — so the state checkpoint lands after every stub through contiguous single-node replaces. Wall-clock stays near one summarize call.
|
||||
- The superseded state checkpoint folds into the next pass's first chunk as ordinary history: no tombstone, no new primitive. Its stub omits it, `history_read` renders it labeled `[prior state checkpoint]`, and its footer travels with the rendered text, keeping every trailing slice reachable through the two-hop chain.
|
||||
- Range selection is frozen-aware: the compactable span begins after the last committed index checkpoint, at the surface head only when none exists. A legacy session's existing head checkpoint is adopted as state-class — its text the merge base, its node folded like any superseded state.
|
||||
- A crash in the summarize phase commits nothing; a crash mid-commit leaves a left-to-right prefix committed, and the resumed pass reads its merge base from the log's latest state-class `compact/summary` event and commits the remaining regions unconditionally — restoring `[stubs…][state][tail]` outranks shrinking.
|
||||
|
||||
### The recall tools
|
||||
|
||||
A new package `@deepseek-ai/dsh-tool-recall` (consumer-only, over the `dsh-session` and `dsh-compact` vocabularies) registers two model-facing tools:
|
||||
|
||||
- `history_read(checkpoint, offset?)` — renders the shadowed span of any checkpoint in the log, including superseded ones, as `User:`/`Assistant:`/`Tool result:` transcript, paginated by a configured budget with a continuation cursor.
|
||||
- `history_search(query, checkpoint?, limit?)` — case-insensitive literal scan over every shadowed span; returns snippets with checkpoint ids and coverage metadata (`scanned`/`matched`/`truncated`). The zero-match hint notes the scan is literal and points at direct `history_read` of a plausible checkpoint.
|
||||
|
||||
Both read `exec.agent.session.events` (the tool-todo access pattern; non-agent callers rejected), render only surface-type message events, and return ordinary `tool/result`s — recalled bytes land at the context tail, logged, so reconstructability holds with no special casing. There is no new storage and no sidecar index: the session log is the archive, `compact/summary` provenance is the index metadata, and the tools are a read path over both. The tool schemas and the package's one system-prompt section are static strings; checkpoint ids reach the model only through footers. The transcript renderer moves from `compact-basic` into `dsh-session`, shared by summarizer and tools.
|
||||
|
||||
### Cache and cost
|
||||
|
||||
The request prefix after a pass is `[system][stubs…][state][tail]`. Frozen stubs are byte-stable across passes, so the miss begins at the token replacing the previous state checkpoint and stays O(new chunks + state + tail) — against position zero today. Recall output lands at the tail, leaving the prefix untouched. Per-pass summarize input is roughly twice today's plus an m·S background term, bounded by a `chunkTokens` floor (a small multiple of the state cap) and a validated `stubTokens`/`chunkTokens` ratio ceiling; a shared-prefix input layout (preamble, then the byte-identical pass-start state, slice content in the tail) lets sibling calls earn cached-rate rereads.
|
||||
|
||||
### Packaging
|
||||
|
||||
The design ships as a new backend `dsh-compact-recallable` on the existing `ctx.compact` seam, enabled by default in the shipped example configs; `compact-basic` remains as the reference implementation and the seam's design twin, in the pattern of the paired LLM adapters. The seam JSDoc's "at most one auto-generated checkpoint, always at the head" clause is relaxed to name both backend behaviors.
|
||||
|
||||
### Relation to in-flight work
|
||||
|
||||
- **Tool-result pruning** (the in-flight pruning service): its replacement nodes carry `sourceEventSeqs`; the same registry fold lists pruned results as recallable. Follow-up scope; neither blocks the other.
|
||||
- **Provider-usage token accounting** (the in-flight move of compaction pressure onto provider-reported usage): supplies the guard's accounting; the implementation stacks after it.
|
||||
- **"Query sessions" backlog item**: the cross-session generalization; this Agent Note scopes to the live session with tool names and rendering chosen so that work extends rather than collides.
|
||||
- **Training**: when to recall is a learned behavior. The deterministic footers and keyword anchors give training a stable target, and recall usage is fully visible in the session log for trajectory export; benchmark and RL design proceed with the post-training side.
|
||||
|
||||
### Follow-ups
|
||||
|
||||
Specified during review, deferred until observation calls for them:
|
||||
|
||||
- Guard degradation ladder (code-only rollup of the oldest stub prefix, footers preserved, rolled-up ids remain recall targets; then one summary after the frozen boundary) — on observed guard livelock or stub-region pressure.
|
||||
- Echo detection on stub outputs (sentence-scale n-grams, short literals exempt, retry then strip) — on observed division-of-labor leakage.
|
||||
- Periodic state refresh from chunk originals — on observed drift in the handoff probe.
|
||||
- `stateFallbackThreshold` (full-detail state prompt below a stub count) — on short-session regression.
|
||||
- Lazy registration of the recall tools — on measured context tax in never-compacting sessions.
|
||||
- Amortized stub drafting at pre-step: as soon as stale-but-uncompacted content accumulates past `chunkTokens`, draft that chunk's stub at the next pre-step (a log-only draft event, written while the chunk's surrounding context is still live) and let the compaction pass commit drafts instead of summarizing in bulk — the deterministic, replay-exact equivalent of background compaction (the Claude Code session-memory pattern; OpenClaw demonstrates the synchronous semantics are identical). Trigger: observed pass latency, or stub-quality gains from drafting near-live proving out.
|
||||
- Split summarizer models; model-chosen chunk boundaries; cross-session recall; semantic search fallback — each behind its own evidence.
|
||||
- Richer `history_search` query forms — regex, and structured queries over logged JSON tool results (sql/jq-style, or agent-authored queries against an indexed store) — on demand from observed search misses; literal matching ships first because the recall path stays a pure function of the log.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Staged delivery** (ship recall tools alone over today's backend; gate the checkpoint split on observed recall usage) — rejected: untrained models under-use any new tool, so the gate would measure training absence rather than design value, while the training side needs the complete mechanism to build environments against; the pre-release window is when persisted-format changes are cheapest; and the cache economics are first-party knowledge, not a hypothesis awaiting telemetry. The implementation still lands as stacked PRs with the recall tools first — construction order, not a decision gate.
|
||||
- **All-frozen full-size summaries, no state checkpoint** — rejected: unbounded permanent-prefix growth, self-accelerating toward thrashing, with nothing left to re-prioritize.
|
||||
- **Pure stubs, no state checkpoint** — rejected: presumes the model knows what it is missing; fails on unknown unknowns.
|
||||
- **LLM aging/consolidation of frozen chunks** — rejected as a routine mechanism: summary-of-summary loss and frozen-prefix churn; the code-only rollup is its surviving form, deferred.
|
||||
- **Full prefix as chunk-summarizer input** — rejected: O(N²); the state document gives the same background at O(state).
|
||||
- **One summarize call emitting all outputs** — rejected: the summarize path has no structured-output enforcement; parsing one free-text response apart is the fragile seam the fail-closed design avoids.
|
||||
- **Model-chosen chunk boundaries** — deferred: parse-and-validate cost against unproven value; chunk policy sits behind config.
|
||||
- **Model-authored pointers** — rejected: pointers must be exact; deterministic assembly is.
|
||||
- **FTS/vector index sidecar** — rejected in-session: the live log is in memory and bounded, a literal scan under budget suffices; an index earns its keep at cross-session scope.
|
||||
- **Semantic search fallback / secondary-model extraction in the recall path** — rejected: an LLM or embedding call there breaks keyless replay determinism; recall stays a pure function of the log.
|
||||
- **Raw events instead of rendered transcript** — rejected: leaks log-only vocabulary and chunk noise; the model reads what a model once saw.
|
||||
- **Doing nothing (resume/fork as recovery)** — rejected: it makes recovery a human act.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Auto-compaction over a long session yields `[stubs…][state][tail]` after every completed pass; prior stubs stay byte-identical across passes; committed stubs never fall inside a later region; the superseded state checkpoint folds without a tombstone, renders labeled, and stays reachable and searchable through the two-hop chain.
|
||||
- Every checkpoint's surface text ends with the deterministic footer; footers round-trip through replay byte-identically; the state checkpoint's provenance records its wider input range.
|
||||
- Nothing commits before all summaries exist and the guard passes on like-for-like accounting; a guard failure commits nothing and does not fail the turn; a mid-commit kill resumed at the next pre-step completes the pass with the state region committed unconditionally, merge base read from the log; a legacy head checkpoint is adopted as state-class.
|
||||
- `history_read` renders any logged checkpoint's span under budget with a working cursor; `history_search` covers every shadowed span with checkpoint-id snippets and coverage metadata, asserted in particular by finding content that exists only in a span shadowed by a superseded state checkpoint — the regression pin for trailing-slice reachability; both reject non-agent callers and never-existing ids or orphaned `compact/start` with typed errors; recalled content appears as ordinary `tool/result`s; request-reconstruction invariants pass over sessions with compaction plus recall; one keyless snapshot scenario covers compact-then-recall end to end; tool schemas and the prompt section are byte-identical across passes.
|
||||
- On the long-horizon bench suite: task success does not regress against `compact-basic` at equal budgets; a handoff-fidelity probe (restate K known decisions and constraints after a pass) scores no worse; recall usage frequency and hit usefulness are reported per run via the dsh bench report pipeline, alongside the stub-directory attention measurement and cache-hit telemetry.
|
||||
- Seam JSDoc, the compaction capability-seam Agent Note, `architecture.md`, and the generated tool, config, persistence, and module-graph catalogs update in the same change; all budgets live in config; new source directories hold per-file 100% coverage with HMR disposal tests.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Recall is a learned behavior**: untrained models will under-use it, and the bench report exists to track the gap while training closes it. Until then the state checkpoint keeps the floor at today's summary quality.
|
||||
- **Unknown unknowns remain**: a detail absent from summaries and keywords draws no recall. Recall converts "unreachable even when suspected" into "reachable when suspected".
|
||||
- **The stub directory occupies attention**: dozens of stable index cards per request may dilute focus; the bench measurement in the acceptance criteria tracks it against `compact-basic`.
|
||||
- **Cost**: per-pass summarize input is roughly twice today's; short sessions sit near today's cost and quality, and the design pays off with session length.
|
||||
- **State drift and division-of-labor leakage** are observable through the handoff probe and stub review; their counters are specified follow-ups.
|
||||
- **Two backends** are a maintenance surface; the seam contract and the shared recall consumer bound it, and the bench comparison decides the default over time.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Agent Note: Claude Code and Codex subagent backends (out-of-process delegation to external coding agents)
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The subagent seam ([the seam Agent Note](../../implemented/feature/2026-06-21-subagent-capability-seam.md)) hosts multiple named providers on `ctx.subagents`, and the ACP backend ([the ACP backend Agent Note](../../implemented/feature/2026-06-22-acp-subagent-backend.md)) proved the seam generalizes across a process boundary; its Future-providers section explicitly named the Codex app-server and the Claude Code Agent SDK as mechanically similar siblings. Those two are the engines actually worth delegating to today: a harness turn should be able to hand a self-contained task to a real Claude Code or a real Codex — a separate product with its own model, tools, and sandbox — and get back one final answer, without the parent deployment leaking its secrets into the child or the child's behavior silently depending on whatever `~/.claude` / `~/.codex` state exists on the host machine.
|
||||
|
||||
## Proposal
|
||||
|
||||
Two sibling provider packages, structural variants of the ACP backend, plus one extraction:
|
||||
|
||||
- `@deepseek-ai/dsh-subagent-claude-code` — drives a Claude Code child through `@anthropic-ai/claude-agent-sdk`'s `query()` (the SDK runs in the parent process and spawns its bundled `claude` CLI as the subprocess). Provider name `claude-code`: the child is the Claude Code *product*, not an Anthropic model adapter — "claude" stays reserved for a future `dsh-llm` adapter.
|
||||
- `@deepseek-ai/dsh-subagent-codex` — spawns `codex app-server` and drives one thread/turn over its JSON-RPC-over-stdio protocol with a hand-rolled newline-JSON client (~200–300 lines) in the package.
|
||||
- `@deepseek-ai/dsh-subagent-process` — a pure library (the `subagent-inprocess` precedent) extracting what `dsh-subagent-acp` already carries and both new backends need: the credential env scrub (`buildChildEnv`), the EOF → SIGTERM → SIGKILL dispose ladder, and new isolated-config-dir helpers (`mkdtemp` create, best-effort remove). The ACP backend migrates onto it; `bash-local`'s sibling copy is left alone to bound the change.
|
||||
|
||||
Both providers copy the ACP backend's seam posture verbatim: fresh child per `start`, exactly one prompt round-trip, capabilities all `false`, `inheritsParentContext: false`, `request.parent`/`request.agentOptions` ignored, `id = SessionId(randomUUID())`, `result` never rejects — child-level failure flattens to a stop reason and the original error goes to `ctx.logger` via an `onError` spec callback. Model exposure is zero new code: `dsh-tool-subagent` is loaded once per provider with a distinct `toolName` (`subagent_claude_code`, `subagent_codex`). No new session events are needed — the only model-visible artifact is the tool result, so reconstructability holds exactly as it did for ACP. To be explicit about the boundary: the session log reconstructs the model-visible transcript, not workspace mutation history — a child granted write access mutates files as an ambient side effect outside the log, exactly as the bash tools and the ACP backend already do; replay reproduces requests, not the disk.
|
||||
|
||||
## Verified interface facts (pinned versions)
|
||||
|
||||
Both integration surfaces were verified against pinned implementations before this proposal — types and bundled source read, keyless spikes run — not from vendor docs alone. The pins are the verification baseline, not a runtime contract: the backends perform no runtime version probe (no `codex --version` gate, no SDK version sniffing). Compatibility is enforced at development time — every dependency bump re-runs the keyless suites against the real load path — and at runtime by failing loudly: a protocol-level surprise settles `error` via `onError`, never a silent misbehavior.
|
||||
|
||||
**`@anthropic-ai/claude-agent-sdk` 0.3.202.** `options.env` REPLACES the child environment (no merge with `process.env`), which is exactly what the scrub needs. `settingSources` defaults to loading ALL filesystem settings — isolation requires explicitly passing `[]`. Result subtypes are `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`. On abort the SDK escalates the CLI child itself: stdin EOF immediately, SIGTERM ~2s later if the child ignores it (observed; no leftover processes) — no bespoke kill fallback needed. `outputFormat: {type: 'json_schema'}` and an `agents` option exist, giving future landing points for the seam's `outputSchema` capability and named subagent types; both are out of scope here.
|
||||
|
||||
**codex CLI 0.142.5, `codex app-server` (v2 vocabulary).** LF-delimited JSON, JSON-RPC 2.0 shapes with the `"jsonrpc"` header omitted.
|
||||
|
||||
- Lifecycle: `initialize{clientInfo}` + `initialized` → `thread/start` (accepts `cwd`, `model`, `sandbox`, `approvalPolicy`, `ephemeral`; succeeds unauthenticated) → `turn/start{threadId, input:[{type:'text',text}]}` returns an `inProgress` turn immediately; the terminal signal is the `turn/completed` notification carrying `Turn{status: completed|interrupted|failed|inProgress, error}`.
|
||||
- Approvals are server-initiated requests — `item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/permissions/requestApproval`, `item/tool/requestUserInput`, `mcpServer/elicitation/request` — answered with `accept`/`decline`-family decisions.
|
||||
- Auth: `account/login/start{type:'apiKey', apiKey}` is a first-class RPC and `account/read` reports `requiresOpenaiAuth` — and an unauthenticated `turn/start` does NOT fail fast (it hangs in retry), so the backend MUST pre-check auth and settle `error` loudly instead of waiting on the turn.
|
||||
- Isolation: `CODEX_HOME` redirection is honored (the `initialize` response echoes it, so tests can assert isolation), and `ephemeral: true` threads leave no session files at all.
|
||||
|
||||
## Isolation and credentials
|
||||
|
||||
Deployments authenticate with API keys only, and the child must not see the host user's Claude Code / Codex configuration: behavior has to be a function of `cordis.yml` alone. Each run gets a fresh `mkdtemp` config dir — `CLAUDE_CONFIG_DIR` for Claude Code (paired with an explicit `settingSources: []`), `CODEX_HOME` for Codex — removed best-effort on dispose; a config field can pin a persistent dir instead. The child env reuses the ACP backend's `buildChildEnv` semantics verbatim via the extraction: the ambient env is forwarded MINUS credential-shaped vars (`/KEY|SECRET|TOKEN/i`), with `config.env` layered on top — so `PATH`, `HOME`, `TMPDIR`, locale, and proxy vars survive and the CLIs run normally, while only credential-shaped ambient vars are scrubbed (`ANTHROPIC_API_KEY` enters explicitly through `config.env` for Claude Code), and the Codex key travels via the `account/login/start` RPC into the isolated `CODEX_HOME` rather than a hand-written `auth.json`.
|
||||
|
||||
## Permission and approval policy
|
||||
|
||||
Instead of collapsing to ACP's single `permission: allow|reject` knob, each backend exposes its engine's native vocabulary as config, with conservative defaults: Claude Code gets `permissionMode` (default `default`) plus `permission: allow|reject` (default `reject`) as the `canUseTool` auto-answer for whatever falls through; Codex gets `sandboxMode` (default `read-only`) and `approvalPolicy` (default `never`) plus the same `permission` fallback for approval requests that still arrive. Defaults are deliberately do-no-harm (the out-of-box child cannot write files); examples demonstrate opening up (`acceptEdits` / `workspace-write`). The mechanical rule: EVERY server-initiated request is settled programmatically and promptly — the enumerated approval/user-input/elicitation requests by the configured policy, an unknown request method with a JSON-RPC method-not-found error response (never left pending), unknown notifications consumed — so no child request can wedge a turn waiting on an answer that will never come. Prompts never reach a human in this cut, matching ACP.
|
||||
|
||||
## StopReason mapping
|
||||
|
||||
Claude Code: `success` → `completed`; `error_max_turns`, `error_during_execution`, `error_max_budget_usd`, `error_max_structured_output_retries` → `error` (aligning with the ACP call on `max_turn_requests`: an unfinished task is not success); generator abort → `aborted`; anything unknown → `error`. Codex: `Turn.status` `completed` → `completed`; `interrupted` → `aborted`; `failed` with `codexErrorInfo: 'contextWindowExceeded'` → `max-tokens`, any other `failed` → `error`; transport/spawn/auth-precheck failure → `error` (or `aborted` if cancel was requested). In both, `cancel()` is the ACP shape: flag + abort/interrupt + a cancel-settled race arm so an uncooperative child cannot stall the result.
|
||||
|
||||
Liveness posture, stated explicitly: teardown timing is config, turn duration is not. Both backends take the dispose ladder's grace periods as defaulted validated config fields (the ACP backend's `disposeEofGraceMs`/`disposeGraceMs` shape, carried by the extraction), but there is deliberately NO turn-duration or startup timeout — matching ACP, liveness during a turn belongs to the caller via `cancel()`/the abort signal, a subagent turn is legitimately minutes long, and the Codex auth precheck removes the one verified guaranteed-hang; a deployment wanting a wall-clock bound cancels from the parent.
|
||||
|
||||
## Testing
|
||||
|
||||
Named at every tier per the root AGENTS.md rule, and de-risked up front:
|
||||
|
||||
- **Keyless unit/integration**, mirroring the ACP spec list per backend (round-trip and output accumulation, every stop mapping, both cancel paths, already-aborted, permission auto-answer under both policies, unknown-message tolerance, bad-command spawn failure, HMR provider cleanup, export shape, isolation assertions on child env and temp-dir removal; Codex adds the auth-precheck failure path). Claude Code's harness is a scripted fake `claude` executable behind `pathToClaudeCodeExecutable` driven by the REAL SDK — a spike already passed end-to-end keyless in 24ms (the fake CLI answers one `control_request/initialize` and speaks plain stream-json, ~40 lines). Codex's harness is a scripted mock app-server subprocess speaking the verified wire protocol, the `mock-acp-server.ts` shape.
|
||||
- **With-key e2e** per backend: the real engine does real file work verified on disk, under a pinned opened-up config so acceptance and the do-no-harm defaults don't collide — `permissionMode: 'acceptEdits'` for Claude Code, `sandboxMode: 'workspace-write'` + `approvalPolicy: 'never'` for Codex; self-skips report exactly what is missing (binary vs key). CI has no secrets, so these run locally per the with-key policy.
|
||||
- **Snapshot**: deferred as `TODO(claude-code-subagent-replay)` / `TODO(codex-subagent-replay)` — the same distinct replay shape the ACP backend deferred ([the per-session replay Agent Note](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md)); the keyless suites carry deterministic coverage meanwhile.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Why not the official `@openai/codex-sdk` instead of a hand-rolled client?
|
||||
|
||||
The dispose ladder and env scrub require owning the child process (spawn args, env, signals, exit await); the SDK hides the process. The wire format is trivial to frame (LF JSON), the shapes are generatable per pinned version (`codex app-server generate-json-schema`), and the repo precedent (`hook-protocol`) is to own thin protocol cores rather than wrap someone's runtime. The SDK would save protocol-evolution maintenance but costs the exact control this backend exists to have.
|
||||
|
||||
### Why not a model-visible `subagent_type` parameter (one Task-style tool)?
|
||||
|
||||
Claude Code's own Task tool puts the subagent type in the model-facing schema, selecting a prompt-plus-toolset persona. Here the choice is between EXECUTION ENGINES, and only the deployer knows which engines have credentials configured — so selection stays deployment config, preserving `dsh-tool-subagent`'s documented one-provider-per-tool contract. A persona-style type selector would be a separate Agent Note against the tool, not the backends.
|
||||
|
||||
### Why not login-state credentials and the user's own config?
|
||||
|
||||
Inheriting `~/.claude` / `~/.codex` (subscription login, user settings, skills, MCP servers) would make child behavior depend on host-machine state and punch an implicit exception through the "credentials enter explicitly via `config.env`, never ambiently" rule the ACP backend and bash executor established. API-key-only plus forced config-dir isolation keeps runs reproducible; deployments wanting shared state can point the config-dir field at a persistent directory deliberately.
|
||||
|
||||
### Why not a driver-injection seam for the Claude Code keyless tests?
|
||||
|
||||
Injecting a fake `query()` would mock our own boundary and leave the real SDK load path untested (the real-over-mock policy in docs/testing.md). The risk that justified considering it — the SDK↔CLI stream-json control protocol being internal — was retired by the spike: the fake-CLI harness works against the real pinned SDK today. If an SDK upgrade breaks the mock, the keyless suite fails the upgrade PR, which is the gate working.
|
||||
|
||||
### Why not ACP adapters (e.g. `claude-code-acp`) reusing the existing backend?
|
||||
|
||||
Community shims wrap both engines in ACP, which would make them "just config" on `dsh-subagent-acp`. But that inserts an unofficial third-party layer between the harness and the engine, erases the native control surfaces this Agent Note exposes (permissionMode, sandboxMode/approvalPolicy, config-dir isolation, apiKey RPC), and trades first-party protocol stability for a shim's release cadence. First-party surfaces — the Agent SDK and the app-server — are the supported integration points.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
On a machine with both engines and keys configured: a REPL-driven model completes one real file task through `subagent_claude_code` and one through `subagent_codex`, the tool result being the child's final answer, with only `tool/call` + `tool/result` in the parent session log. Keyless suites pass at 100% per-file coverage in a credential-less environment, asserting isolation (scrubbed child env, no temp config dirs left after dispose) and that child behavior is unchanged by the presence or absence of `~/.claude` / `~/.codex`. Cancelling a parent turn quiesces both backends in bounded time with no leftover child processes. E2e suites self-skip cleanly, naming the missing prerequisite.
|
||||
|
||||
## Risks
|
||||
|
||||
- `codex app-server` is CLI-flagged experimental and its v1/v2 vocabularies coexist; the client pins 0.142.5, implements v2 only, and consumes unknown methods/notifications without crashing, but a future codex bump can still force rework (regenerate schemas and re-run the keyless suite on every bump — the development-time enforcement behind the no-runtime-version-probe stance above).
|
||||
- The Claude Code fake-CLI mock rides an internal protocol: any SDK upgrade must go through the keyless suite, and a breaking control-protocol change means reworking the mock (fallback: the driver-injection seam rejected above becomes the escape hatch).
|
||||
- The SDK's optionalDependencies weigh ~280MB per platform — accepted, and confined to the one backend package.
|
||||
- The SDK's SIGKILL branch beyond EOF→SIGTERM was not observed and is trusted; e2e keeps a no-leftover-process assertion.
|
||||
- Codex is a deployment prerequisite (no npm-bundled binary); a missing or incompatible binary surfaces as a loud spawn/protocol `error`, not a version probe.
|
||||
- Every run pays a fresh child process and only the final answer surfaces — thoughts, tool cards, and usage are consumed and dropped; pooling, intermediate-progress surfacing, `sendMessage`/`resume`, `outputSchema` via the SDK's `outputFormat`, and named subagent types via the SDK's `agents` option are all deliberate deferrals.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: Interactive side sessions and merge-back
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
A user may want to explore a question from a live session without changing its main context. Existing primitives do not expose that product shape: [session-store fork](../../implemented/feature/2026-06-30-session-store-fork-api.md) creates an unattached session, while [fork subagents](../../implemented/feature/2026-06-21-subagent-capability-seam.md) are model-driven tasks whose transcript collapses into one tool result. Neither gives the user a separate conversation, and neither records a conclusion back into the parent with provenance.
|
||||
|
||||
## Proposal
|
||||
|
||||
A **side session** is an ordinary live session forked at the source's last completed turn, attached to its own agent, framed as a read-only advisor, and able to **merge back** one condensed note.
|
||||
|
||||
- **Fork and attach:** create the child with the parent's balanced completed-turn prefix and stamp `parentSession` and `seedLength` in its metadata. This composes `ctx.agents.create({ seed, meta })`; it adds no core service or session-store method.
|
||||
- **Advisor framing:** inject one plugin-sourced `context/message` after creation that tells the child to explain without mutating or continuing the task. Keeping the system prompt byte-identical preserves the provider prefix cache over inherited history.
|
||||
- **Merge-back:** ask the child for a length-capped handback, then inject one plugin-sourced `context/message` into the parent. The next parent request sees it at its logged position, preserving replay and [request reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) without a new session event.
|
||||
- **Presentation:** invocation, session switching, and handback rendering belong to the first client-owned surface. This Agent Note specifies only the surface-independent mechanics.
|
||||
|
||||
Rewind productization, session-tree views, a model-facing side-session tool, and `forkName`/`mergedInto` metadata are out of scope. A live-adapter spike validated source-log isolation, inherited context, a multi-turn child exchange, and merge-back visibility in the parent's next turn.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Use the subagent seam:** rejected because side sessions are user-driven, client-visible, and may outlive a parent turn; subagents are model-driven runs returning one tool result.
|
||||
- **Change the child system prompt:** rejected by default because any byte change invalidates the prefix cache from token zero. Deployments may still prefer that stronger separation.
|
||||
- **Add `sidechat/*` events:** deferred because a sourced `context/message` already provides durability, provenance, and replay. A dedicated event is justified only by a surface that needs distinct rendering.
|
||||
- **Bind a protocol surface now:** rejected because current UIs are client-owned. Live presentation must eventually derive from the durable message so replay renders the same record.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Forking leaves the source untouched and creates a child with the balanced completed-turn prefix, `parentSession`, `seedLength`, and a byte-identical system prompt.
|
||||
- Advisor framing adds exactly one plugin-sourced `context/message` at the head of the child's appended history, rather than changing its system prompt.
|
||||
- Merge-back adds exactly one length-capped `context/message` with source `plugin: sidechat`; the next parent request and replay see it at the same position.
|
||||
- Parent and child run concurrently without log or stream cross-talk.
|
||||
- Unit tests cover fork/attach and merge-back; snapshot coverage lands with the first bound surface.
|
||||
|
||||
## Risks
|
||||
|
||||
- Read-only behavior is advisory until a `tools/pre-execute` deny gate enforces it; [the interception seam](../../implemented/feature/2026-06-30-interception-seams.md) can add that gate without changing these mechanics.
|
||||
- A compacted source forks its compacted view, so a bound surface should disclose that the child inherits summaries rather than replaced turns.
|
||||
- Repeated handbacks consume parent context. The per-merge length cap bounds each note; later consolidation belongs to compaction.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: SQLite FTS5 session search
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The exact-read `ctx.sessionQuery` service deliberately has no derived index. Large persisted histories need full-text search without scanning every event on every query, while current live sessions need an overlay newer than the last durability checkpoint. Search also needs concrete ranking, snippets, pagination, cancellation, and rebuild behavior.
|
||||
|
||||
Splitting those concerns across a speculative provider coordinator and a database implementation would create two coupled reconciliation state machines. The first real implementation should own the source observation, extraction, SQLite transaction, generation, and query as one lifecycle.
|
||||
|
||||
## Proposal
|
||||
|
||||
Add `@deepseek-ai/dsh-session-query-sqlite` beside the exact-read package. The package will expose a search service or extend the family with the smallest API required by its actual consumers; phase one does not pre-commit a provider-registration protocol. It will depend on `ctx.sessions` and optional `ctx.sessionPersistence`, own a separate derived SQLite database, and reuse the canonical `foldSurface()` classification.
|
||||
|
||||
The implementation owns one serialized reconciliation/DB transaction state machine. A transaction observes authoritative persisted metadata and live snapshots, extracts semantic documents, updates derived tables, advances relevant cursor generations, and executes or enables the corresponding query. No second service maintains parallel fingerprints, dirty flags, live-id sets, or invalidation generations.
|
||||
|
||||
Persisted documents survive restarts. Live overrides are connection-local and shadow the persisted rows for the same session, then disappear when the live owner or database closes. The derived database remains separate from canonical persistence so index reset, corruption, tokenizer changes, and schema churn cannot endanger durable conversation logs.
|
||||
|
||||
## Search semantics to decide with implementation
|
||||
|
||||
The implementation must define both cross-session and within-session scopes from executable use cases. Each searchable event is one document with session metadata, event metadata, surface classification, normalized semantic text, and a bounded plain-text snippet. Session results group by their strongest matching event; numeric backend scores remain private.
|
||||
|
||||
Search returns content-bearing result records rather than metadata-only headers. Chainable filters operate on that exact result shape and are designed and implemented with the search API instead of becoming a provider-specific pre-ranking contract. Query syntax is treated as data. Ordering includes stable tie fields. Opaque cursors bind to normalized request shape and the smallest relevant generation; unrelated session changes should not invalidate a within-session cursor. Cancellation must stop caller waiting and interrupt SQLite work where the runtime permits.
|
||||
|
||||
Tokenizer choice remains an implementation experiment. FTS5 trigram supports substring recall but rejects useful terms shorter than three characters and increases index size; the proposal must benchmark that tradeoff against the default Unicode tokenizer before making it contract.
|
||||
|
||||
## Extraction and reconciliation
|
||||
|
||||
The package starts with first-party semantic extraction for messages, reasoning, tool calls/results, blocked prompts, context, steering, todos, and error/status detail. Structural events and stream chunks contribute no document. Unknown declaration-merged event/content types remain non-searchable unless a real extension consumer demonstrates the need for a public extractor registry.
|
||||
|
||||
Reconciliation may use stable fingerprints to avoid rewriting unchanged persisted sessions, but the database package owns their calculation and storage. It must never report a row current when source observation or extraction failed. Provider-schema mismatch may reset only the derived database; ordinary source changes use transactional upsert/delete. Mounted but unreadable persistence fails affected searches without affecting canonical writes or known live exact reads.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Add FTS tables to the canonical persistence database** — rejected because a rebuildable index must not share the authoritative log's schema/reset/failure boundary.
|
||||
- **Reintroduce phase-one provider coordination** — rejected because there is one planned implementation and no evidence for a stable multi-provider seam.
|
||||
- **Persist live overrides immediately** — rejected because live events are not canonical until the existing checkpoint commits.
|
||||
- **Return BM25 scores** — rejected because provider-specific numeric scales are unstable across corpus changes.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Restart tests cover unchanged, new, changed, and deleted persisted sessions without rebuilding the whole index.
|
||||
- Reopening preserves persisted rows and removes live rows; live rows shadow and then reveal their persisted base.
|
||||
- Tests cover both search scopes, content-bearing results, chainable result filters, surface defaults, snippets, escaping, deterministic ties, pagination, scoped stale cursors, cancellation, dynamic persistence mount/unmount, and recovery after a failed transaction.
|
||||
- A schema mismatch resets only the derived database.
|
||||
- A keyless end-to-end test combines a real persistence backend with the real SQLite search package.
|
||||
- The Agent Note is amended to the measured tokenizer and public API actually implemented before moving to `implemented/`.
|
||||
|
||||
## Risks
|
||||
|
||||
A single owner is simpler but initially less reusable than a provider-neutral seam. That is intentional: a second real backend can reveal what to extract. SQLite runtime differences can affect FTS ranking and snippets, so tests must pin only contract-controlled ordering and presentation. The separate database adds configuration and lifecycle work, but preserves the canonical store's safety boundary.
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Stream workflow progress through tool calls
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The workflow engine intentionally emits balanced `workflow/*` observation events for run, phase, narration, and child-agent progress, but no production consumer presents them. Editors therefore show one pending workflow tool card until the final result even while the engine already reports which phase is active, what the script logged, and which children started or settled. The [dynamic-workflows decision](../../implemented/feature/2026-07-05-dynamic-workflows.md) explicitly reserves ACP progress UI for this event stream.
|
||||
|
||||
Making `dsh-acp` listen to workflow events directly would invert the capability boundary: the generic UI bridge would depend on an optional workflow package and special-case one tool name. The tool pipeline already owns the routing facts a live update needs—agent and call id—but exposes only pure pending/final presenters, so a long-running tool has no provider-neutral way to report transient UI state between them.
|
||||
|
||||
## Proposal
|
||||
|
||||
Add a live progress channel to `dsh-tools`. The registry-owned `ToolExecution` gains `reportProgress(view): boolean`, where `view` is a detached provider-neutral generic progress snapshot containing an optional replacement title and UI-facing content blocks. Progress cannot change the call's args-derived card tag, kind, raw input, locations, terminal intent, or diff intent; it updates only the live title/content within the presentation chosen up front. While the execution is active, the method validates and snapshots the view, then dispatches a contained, agent-scoped `tools/progress` observation carrying the authoritative execution identity and snapshot. Once final-result processing begins it returns `false` and emits nothing, so a late asynchronous reporter cannot overwrite a terminal card. Observer exceptions are logged and cannot fail the tool.
|
||||
|
||||
`dsh-acp` consumes `tools/progress` generically. It resolves the execution's agent through its existing agent-to-session map and emits an in-progress `tool_call_update` for the same call id. Because reporting is available only inside the tool execution pipeline, the durable `tool/call` and its ACP `tool_call` always precede the first update; closing the reporter before `tools/result` ensures no progress update follows the completed/failed card. Progress is live UI state rather than model input or durable history: session replay continues to reconstruct the pending and final cards from `tool/call` and `tool/result` without replaying transient updates.
|
||||
|
||||
`dsh-tool-workflow` becomes the first producer. Each tool execution installs a compact event capture before calling `ctx.workflows.start()`, because a valid engine may emit progress synchronously inside `start()`. Until the call returns, the capture reduces observed events into candidate states keyed by `WorkflowRunInfo.id`; it then selects the returned `WorkflowRun.id`, discards other candidates, reports the accumulated snapshot, and routes later matching events directly. If `start()` throws, the capture is disposed and its candidates are dropped. This preserves engine swappability without adding observer correlation to `WorkflowStartRequest` or requiring progress to wait until `start()` returns.
|
||||
|
||||
The reducer consumes the existing start, phase, log, agent-start, agent-end, and end events, reporting a replacement snapshot with the current phase, latest log line, active child labels, and completed/failed/cancelled counts. It does not accumulate a narration transcript; settled children leave the active set and become counters. `workflow/end`, tool settlement, or plugin disposal removes the reducer entry and event capture. The six workflow events, their metadata, paired child lifecycle, run handle, cancellation channels, and observer containment remain unchanged; third-party observers can continue consuming them directly.
|
||||
|
||||
Update the tool execution/presentation docs, generated event and API catalogs, workflow package docs, and the workflow data-structure catalog. ACP integration coverage must exercise the real workflow tool and worker seam with a scripted model boundary; the primary ACP snapshot suite adds one workflow-progress scenario because this changes the editor-facing transcript.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Delete the workflow observation surface.** Rejected in [the collapse-workflow simplification](../../rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md): the events and their balanced lifecycle are intentional, and the missing piece is a consumer.
|
||||
|
||||
**Teach ACP about workflows directly.** This could map `WorkflowRunInfo` to a session and card, but it would make the generic bridge depend on an optional capability and bypass the rule that tools own presentation intent. A tool-progress channel solves the same routing problem for every long-running tool.
|
||||
|
||||
**Persist every progress update as a session event.** That would make live narration replayable, but it would permanently enlarge logs with state whose authoritative durable outcome is already the tool call/result pair. If resumable workflow progress becomes a product requirement, it needs a workflow-journaling design rather than UI snapshots disguised as durable facts.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `ToolExecution.reportProgress()` is registry-owned, agent-scoped, snapshotting, observer-contained, and returns `false` without dispatch after terminal processing starts.
|
||||
- ACP routes progress to the correct call in the correct live session; concurrent workflows in different sessions cannot cross-talk, and no `tool_call_update` appears before its `tool_call` or after its terminal update.
|
||||
- Workflow progress shows the current phase, latest log line, active children, and outcome counts while preserving all existing `workflow/*` events and run semantics; a seam test engine that emits start, phase, log, child, and end events synchronously inside `start()` loses none of that reducer state.
|
||||
- Cancellation, worker death, tool failure, session close, and plugin disposal release reducer state; replay emits only the durable pending/final card pair.
|
||||
- Unit, workflow integration, ACP integration, snapshot, typecheck, coverage, doc-sync, module-graph, build, and hygiene gates pass.
|
||||
|
||||
## Risks
|
||||
|
||||
This adds a public live-progress method and event to the tool seam, so implementations must keep the active/terminal boundary exact and detach snapshots before observers see them. The pre-start capture can briefly observe unrelated workflow runs, so it holds only compact candidate state keyed by run id and drops every non-matching candidate as soon as `start()` returns. A workflow can emit many progress changes; the bounded reducer avoids transcript growth but still sends one UI update per meaningful event after correlation. If measured clients need coalescing, it must be a defaulted validated bridge configuration rather than a hardcoded throttle. Transient progress intentionally disappears on replay, so the final tool result remains the only durable workflow card content.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-14-sdk-developer-projects.md: 1be9abcad1e51a1b9a1406f21ce60073427576e0
|
||||
2026-07-14-sdk-developer-projects.zh.md: a8ba1d658f78484a46a7148a4e2ff1b073a3e9f2
|
||||
@@ -0,0 +1,167 @@
|
||||
# Agent Note: Developer-owned SDK projects
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-14-sdk-developer-projects.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
DeepSeek Harness composes features through Cordis plugins, but building a runnable project from an empty directory still requires a developer to understand npm dependencies, the `cordis.yml` plugin set, environment variables, TypeScript builds, local-plugin workspaces, and runtime entrypoints together. These manual steps constrain one another: omitting any one can produce a project that installs but cannot be developed, develops but cannot be built, or builds but cannot start.
|
||||
|
||||
A one-shot generator reduces only the initial creation cost. If the generated result is hidden inside a preset or an uneditable CLI, advanced developers cannot reshape the plugin tree, change Cordis plugin config, or add project-specific behavior. If a generated project immediately leaves tool management altogether, developers must again maintain consistency across all npm dependencies and Cordis plugin config themselves.
|
||||
|
||||
Initial creation and later configuration address the same builtin feature set. When those workflows maintain separate feature lists, feature options, and npm dependencies, new Cordis plugins, npm packages, and Cordis plugin config changes make them diverge. Projects also need an ordinary local-plugin development path that participates in development, build, and start flows.
|
||||
|
||||
## Proposal
|
||||
|
||||
The SDK creates an ordinary, explicit TypeScript/Cordis project owned by its developer. `cordis.yml` is the only runtime plugin tree; development and production read the same file. The generated `package.json`, `cordis.yml`, TypeScript entrypoint, build configuration, and `plugins/*` remain directly editable instead of being hidden behind a preset.
|
||||
|
||||
The only developer product entrypoints are `npm create @deepseek-ai/sdk` and the `dsh-sdk` commands. The initializer performs initial creation, `dsh-sdk config` manages SDK-recognized builtin features afterward, and `dsh-sdk dev`, `dsh-sdk build`, and `dsh-sdk start` own development, build, and startup; this phase provides no `dsh-sdk create`. Create and config consume one manually authored feature definition, so each feature has one source for its feature options, npm dependencies, Cordis config entries, related files, and inspection rules. The [SDK project editing architecture](../architecture/2026-07-15-sdk-project-editing-architecture.md) defines terms such as feature and feature option.
|
||||
|
||||
The SDK offers interaction for feature selection and finite feature options only; it does not turn arbitrary Cordis plugin config into a generic form. A feature collects the small number of dedicated inputs required by its feature options. All other Cordis plugin config remains in `cordis.yml`, with comments documenting common edits, for direct developer control.
|
||||
|
||||
## Developer workflow
|
||||
|
||||
Initial creation collects information in an order where earlier answers determine later questions: target directory and package identity, model provider and credentials, run interface, builtin features and feature options, an optional local plugin, package manager, and whether to install npm dependencies and build. Command-line arguments suppress questions they already answer. Create and config require an interactive TTY in this phase, and cancelling creation writes nothing to the target directory.
|
||||
|
||||
```sh
|
||||
npm create @deepseek-ai/sdk my-agent
|
||||
cd my-agent
|
||||
npm exec dsh-sdk dev index.ts
|
||||
npm exec dsh-sdk config
|
||||
npm exec dsh-sdk build
|
||||
npm exec dsh-sdk start index.js
|
||||
```
|
||||
|
||||
Create rejects every target path that already exists. After committing the project files, the CLI asks whether to install npm dependencies and build. An install or build failure preserves the generated project and prints commands that can retry the failed work.
|
||||
|
||||
Create also offers one `none / plugin / tool` choice. `plugin` creates a fixed `plugins/plugin` Cordis plugin, while `tool` creates a fixed `plugins/tool` model-facing tool; one project creation includes at most one local plugin. The operation updates the workspace, root npm dependency, TypeScript reference, build configuration, and `cordis.yml` together, and any pre-write validation failure leaves the project absent.
|
||||
|
||||
## Features supported during creation
|
||||
|
||||
The table is the developer-visible support set for this phase. A `required` feature is always present but may still offer finite feature options; a `default` feature is preselected in the feature tree; an `optional` feature is selected explicitly. The table describes the product support set, while the runtime registry remains the implementation source of truth.
|
||||
|
||||
| Feature | Create state | Feature options | Constraints and relationships |
|
||||
|---|---|---|---|
|
||||
| `provider` | required | `deepseek` (default) / `custom` | DeepSeek collects an API key; custom also collects a base URL, and a CLI option may override the model name |
|
||||
| `app` | required | `stdio` (default) / `acp` / `embed` | Selects the run interface |
|
||||
| `spine` | required | `default` | Timer, the LLM seam, session storage, system prompt, the tool registry, the agent registry, and the agent loop |
|
||||
| `bash` | required | `local` (default) / `sandbox` | The two feature options are exclusive and independent of the run interface, and both install the model-facing bash tool; sandbox installs the local sandbox provider and sandboxed bash backend |
|
||||
| `persistence` | required | `jsonl` (default) / `sqlite` | Every project selects exactly one persistence backend |
|
||||
| `hmr` | default | `default` | Loads `@cordisjs/plugin-hmr`; dev and start both enable it with the plugin defaults |
|
||||
| `fs` | default | `local` | Installs the local filesystem, policy, and model-facing tools; the process sandbox does not confine in-process fs tools |
|
||||
| `todo` | default | `default` | Provides the `todo_write` tool |
|
||||
| `skill` | default | `default` | Installs the skill registry, the local skill provider, and the model-facing skill tool |
|
||||
| `web` | optional | `deepseek` (default) / `exa` / `perplexity` / `fetch-only` | Search feature options are exclusive; Exa and Perplexity collect their API keys; timeout policy is recommended |
|
||||
| `subagent` | optional | `spawn` (default) / `fork`, multiple | This phase provides only in-process backends |
|
||||
| `workflow` | optional | `workerthread` | Requires the subagent `spawn` feature option |
|
||||
| `compact` | optional | `basic` | Uses SDK-provided context-compaction parameters |
|
||||
| `hooks` | optional | `claude` (default) / `codex`, multiple | Each feature option creates a separate editable configuration file |
|
||||
| `guard` | optional | `repeat-tool` | Provides repeated-tool-call reminders |
|
||||
| `timeout-policy` | optional | `default` | Applies a uniform policy to tools that declare timeout budgets |
|
||||
| `ask-user` | optional | `default` | Provides the `ask_user_question` tool; only `acp` and `stdio` can select it because those two feature options provide the injected user-interaction service |
|
||||
|
||||
Both `bash` feature options apply to ACP, stdio, and embed and are not selected by the run interface. The sandbox feature option writes no active config key and therefore keeps `dsh-bash-sandbox`'s `read-only` default. Generated `cordis.yml` includes a commented example that developers can change explicitly to `workspace-write`:
|
||||
|
||||
```yaml
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-sandbox'
|
||||
# Uncomment to allow writes under the project workspace.
|
||||
# config:
|
||||
# mode: workspace-write
|
||||
# workspaceRoot: !!js process.cwd()
|
||||
```
|
||||
|
||||
Feature contributions reference only single-plugin npm packages and never bundle packages such as `agent-spine-demo`, `stdio-demo`, or `acp-demo`. Plugins outside the table are not managed by create in this phase; advanced developers may still compose them by editing the ordinary project files directly.
|
||||
|
||||
## Generated project
|
||||
|
||||
With default answers, an npm project uses the DeepSeek provider, the stdio interface, local bash, JSONL persistence, and the preselected hmr, fs, todo, and skill features. Its initial tree is:
|
||||
|
||||
```text
|
||||
my-agent/
|
||||
├── .env
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── cordis.yml
|
||||
├── index.ts
|
||||
├── package.json
|
||||
├── tsconfig.base.json
|
||||
├── tsconfig.json
|
||||
└── tsdown.config.ts
|
||||
```
|
||||
|
||||
`.env.example` always exists, and the SDK keeps its placeholders aligned with the current feature set. A gitignored `.env` is also created when a secret is captured or the developer confirms an empty credential to fill later. The SDK only appends differently named variables that are not already present in `.env` and never updates or removes existing contents. Feature-option changes may remove obsolete `.env.example` placeholders, while old credentials remain in `.env` for the developer to manage. pnpm and Yarn projects add their required workspace files, but do not fork the runtime plugin tree or TypeScript entrypoint.
|
||||
|
||||
Generated `package.json` provides the following scripts. `dev`, `build`, `start`, and `config` invoke `dsh-sdk`, while `typecheck` invokes TypeScript directly:
|
||||
|
||||
| Script | Behavior |
|
||||
|---|---|
|
||||
| `dev` | Run `dsh-sdk dev index.ts`, registering development-time resolution for TypeScript and local workspace plugins |
|
||||
| `build` | Run `dsh-sdk build`, invoking the project's installed tsdown for the root entrypoint and `plugins/*` packages |
|
||||
| `typecheck` | Run `tsc -b` directly |
|
||||
| `start` | Run `dsh-sdk start index.js`, starting the built entrypoint without an implicit build |
|
||||
| `config` | Run `dsh-sdk config` to edit the current project's feature tree |
|
||||
|
||||
`dsh-sdk start` and `dsh-sdk dev` accept a module target and forward arguments after `--` unchanged to the project entrypoint. Generic argument parsing uses Node `parseArgs()` with zero schema: valued flags use `--key=value`, bare flags become `true`, and `--no-*` becomes `false`.
|
||||
|
||||
- Stdio projects pass the selected model through `--model=<name>` and create or resume an agent according to optional `--resume=<session-id>`;
|
||||
- ACP uses protocol `session/load`
|
||||
- Embed uses the model written into the generated code.
|
||||
|
||||
Each feature-owned Cordis config entry keeps its developer-editable Cordis plugin config and explanatory comments in `cordis.yml`. When `dsh-sdk config` changes other features, it preserves unknown fields, formatting on untouched nodes, and comments. HMR is an ordinary leaf config entry: when the feature is selected, dev and start load the same watcher, and the command does not change the plugin tree implicitly.
|
||||
|
||||
## Post-creation configuration
|
||||
|
||||
`dsh-sdk config` requires only readable root `package.json` and `cordis.yml` files in the current directory. It inspects standard features and their current feature options, expresses the final desired state through one feature tree, and shows feature changes and affected files before Review & Apply.
|
||||
|
||||
`dsh-sdk config` can install missing features, enable or disable installed features, and switch finite feature options. Required features cannot be removed. An npm dependency change runs the project package manager's install once after the file commit; installation failure does not roll back committed project files.
|
||||
|
||||
The SDK modifies only Cordis config entries, config keys, npm dependencies, `.env.example` placeholders, and owned files explicitly owned by a feature. Updating the same feature option preserves unknown config keys in its Cordis config entries. Handwritten and third-party plugins support enable and disable by stable ID only. When a known feature has been edited into an incomplete, ambiguous, or otherwise unreadable shape, `dsh-sdk config` displays diagnostics and refuses automatic changes until the developer repairs it manually.
|
||||
|
||||
One config session accumulates every change in an in-memory working copy. Before Apply, it validates feature relationships, resource conflicts, and document shapes, then compares each affected existing file with the text read when the session opened. Validation failure or an external edit causes zero writes. Once physical writes begin, the SDK does not provide cross-file transactional rollback.
|
||||
|
||||
## Maintenance model
|
||||
|
||||
The SDK curates its builtin support set instead of exposing npm packages automatically by npm dependency name or directory convention. One feature may compose several Cordis config entries, feature options may share resources, and a feature option may declare a feature requirement on another feature or a specific feature option. Adding an ordinary feature or feature option does not require changes to both create and config command workflows.
|
||||
|
||||
## Future work
|
||||
|
||||
- `dsh-sdk add [package-spec]` unifies local-plugin creation with external Cordis plugin installation: without a package or repository source it creates a local plugin/tool, while a supplied source adds the npm dependency and `cordis.yml` config entry; the source model leaves room for GitHub repositories and other extensions
|
||||
- Non-interactive create/config: both workflows require a TTY in this phase and provide no complete input contract for automation
|
||||
- More feature-specific inputs: this product surface exposes only finite feature options, secrets, and a few dedicated values in this phase rather than a generic parameter interface for Cordis plugin config
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**An opaque preset or generator-owned project.** This shortens initial creation but hides the real plugin tree and build boundaries, prevents advanced developers from composing Cordis plugins directly, and makes project behavior depend on the CLI version rather than committed project files.
|
||||
|
||||
**A one-shot generator only.** Leaving all later maintenance manual redistributes feature requirements, feature-option switches, and multi-file updates. A config workflow over the shared registry retains continuing management for generated projects.
|
||||
|
||||
**Separate `cordis.yml` files for development and production.** Two plugin trees mean a successful development run does not demonstrate that production loads the same features. Dev adds only TypeScript and local-workspace resolution; runtime configuration remains singular.
|
||||
|
||||
**A generic form for arbitrary Cordis plugin config.** Cordis plugin config contains nested structures, expressions, and plugin-specific semantics. A generic form would become a second incomplete schema. The SDK manages finite feature options and dedicated secrets, while developers continue to edit complex config directly.
|
||||
|
||||
**A private local-plugin discovery protocol.** Ordinary package-manager workspaces, root npm dependencies, TypeScript references, and Cordis config entries already express the complete relationship. Another discovery protocol would create hidden state understood only by the SDK.
|
||||
|
||||
**A `dsh-sdk create` command for existing projects.** Create already provides one editable local-plugin skeleton, and later plugins can use ordinary workspace and Cordis mechanisms manually. A parallel command would add a second scaffolding product surface without adding composition functionality.
|
||||
|
||||
**Automatically expose every new Cordis plugin as a builtin.** An npm package cannot say how several plugins compose into one product feature, nor can it derive exclusivity, feature requirements, secrets, interface applicability, or security constraints. The support set requires human curation; automation is suitable only for checking whether candidates have been classified.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `npm create @deepseek-ai/sdk` collects project identity, provider, interface, features, an optional local plugin, package manager, and installation choice in the documented order, and cancellation leaves the target path absent
|
||||
- A default npm project has the documented tree and `dev`, `build`, `typecheck`, `start`, and `config` scripts, with dev and start sharing one `cordis.yml`
|
||||
- Create offers the documented features and feature options; local and sandbox bash are exclusive with local as the default, the sandbox Cordis config entry retains the editable commented config example, and HMR is selected by default and loaded by both dev and start
|
||||
- Create's `plugin` or `tool` choice creates at most one fixed-name local plugin and atomically updates its files and root-project relationships; this phase provides no `dsh-sdk create`
|
||||
- `dsh-sdk config` reads the same support set from an existing project, installs, enables, disables, and switches supported feature options, preserves unknown config and comments, and refuses to modify inconsistent config
|
||||
- `.env.example` reflects variables required by the current features; `.env` only appends missing differently named variables and never updates or removes existing contents
|
||||
- npm, pnpm, and Yarn workspaces install, build, and start; local plugins resolve from source under dev and from built output under start
|
||||
|
||||
## Risks
|
||||
|
||||
- Developers can edit a builtin into a shape the registry cannot recognize; the SDK stops automating that feature instead of guessing and overwriting config
|
||||
- Pre-write validation and external-edit detection do not provide transactional rollback once multi-file writes begin; an I/O failure can leave a partial commit requiring manual repair
|
||||
- The sandbox feature option depends on an available local sandbox backend for the target platform; an unavailable backend must fail closed instead of falling back to unsandboxed execution
|
||||
- HMR retains its filesystem watcher and hot-reload behavior under production start; this is the result of an explicit plugin choice, not an implicit development-only service
|
||||
- The append-only `.env` policy retains credentials that are no longer used; the SDK does not decide when user-owned secret data is safe to delete
|
||||
@@ -0,0 +1,167 @@
|
||||
# Agent Note: 开发者拥有的 SDK 工程
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-14-sdk-developer-projects.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
DeepSeek Harness 通过 Cordis 插件对功能进行组合,但从空目录开始搭建一个可运行工程仍要求开发者同时理解 NPM 依赖、`cordis.yml` 插件组、环境变量、TypeScript 构建、本地插件 workspace 和运行入口。手工步骤之间存在约束,漏掉任意一处都会得到能够安装却无法开发、能够开发却无法构建,或能够构建却无法启动的工程。
|
||||
|
||||
一次性生成器只能降低首次创建成本。若生成结果隐藏在 preset 或不可编辑的 CLI(命令行界面)内部,高级开发者无法调整插件树、修改 Cordis 插件配置或增加项目特有行为;若创建后的工程完全脱离工具管理,开发者又必须重新承担所有 NPM 依赖和 Cordis 插件配置的一致性工作。
|
||||
|
||||
初始创建和后续配置面对同一组内置功能。两条流程各自维护功能列表、功能选项和 NPM 依赖时,新增 Cordis 插件、NPM 包或调整配置会使二者逐渐分叉。工程还需要一条普通的本地插件开发路径,参与开发、构建和启动流程。
|
||||
|
||||
## 提案
|
||||
|
||||
SDK 创建一个普通、显式且归开发者所有的 TypeScript/Cordis 工程。`cordis.yml` 是唯一的运行时插件树;开发和生产读取同一份文件。工程中的 `package.json`、`cordis.yml`、TypeScript 入口、构建配置和 `plugins/*` 均可直接编辑,SDK 不把它们封装成不可见的 preset。
|
||||
|
||||
开发者产品入口只有 `npm create @deepseek-ai/sdk` 和 `dsh-sdk` 命令。前者负责首次创建,`dsh-sdk config` 在创建后管理 SDK 能识别的内置功能,`dsh-sdk dev`、`dsh-sdk build` 与 `dsh-sdk start` 负责开发、构建和启动;本期不提供 `dsh-sdk create`。create 与 config 使用同一份人工编写的功能定义,因此一项功能的功能选项、NPM 依赖、Cordis 配置项、相关文件和识别规则只有一个来源。功能、功能选项等名词由 [SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md) 的术语表定义。
|
||||
|
||||
SDK 只为功能选择和有限功能选项提供交互,不尝试把任意 Cordis 插件配置变成通用表单。功能选项所需的少量专用输入由所属功能收集;其余 Cordis 插件配置留在 `cordis.yml` 中,并通过注释指明常用改法,由开发者直接修改。
|
||||
|
||||
## 开发者流程
|
||||
|
||||
首次创建按会影响后续问题集合的顺序收集信息:目标目录与 package 身份、模型提供方与凭据、运行接口、内置功能与功能选项、可选本地插件、包管理器,以及是否安装 NPM 依赖并构建。命令参数已提供的答案不重复询问;本期 create 和 config 都要求交互式 TTY,取消创建时不写入目标目录。
|
||||
|
||||
```sh
|
||||
npm create @deepseek-ai/sdk my-agent
|
||||
cd my-agent
|
||||
npm exec dsh-sdk dev index.ts
|
||||
npm exec dsh-sdk config
|
||||
npm exec dsh-sdk build
|
||||
npm exec dsh-sdk start index.js
|
||||
```
|
||||
|
||||
create 拒绝任何已经存在的目标路径。工程文件提交成功后,CLI 询问是否安装 NPM 依赖并构建;安装或构建失败时保留生成结果,并打印可以重新执行的命令。
|
||||
|
||||
create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `plugins/plugin` 的 Cordis 插件,`tool` 固定生成 `plugins/tool` 的模型工具;一次创建至多包含一个本地插件。生成操作同时更新 workspace、根 NPM 依赖、TypeScript reference、构建配置和 `cordis.yml`,任何写入前校验失败都不创建工程。
|
||||
|
||||
## 创建时支持的功能
|
||||
|
||||
下表是本期 create 面向开发者展示的支持集。`required` 始终存在但仍可切换有限功能选项;`default` 在选择树中预选;`optional` 由开发者主动选择。表格说明产品支持集,运行时注册表是实现的事实源。
|
||||
|
||||
| 功能 | create 状态 | 功能选项 | 限制与关系 |
|
||||
|---|---|---|---|
|
||||
| `provider` | required | `deepseek`(默认)/ `custom` | DeepSeek 收集 API key;custom 另收集 base URL,模型名可由 CLI 参数覆盖 |
|
||||
| `app` | required | `stdio`(默认)/ `acp` / `embed` | 选择运行接口 |
|
||||
| `spine` | required | `default` | timer、LLM seam、会话存储、系统提示词、工具注册表、agent 注册表,以及 agent loop |
|
||||
| `bash` | required | `local`(默认)/ `sandbox` | 两个功能选项互斥、与运行接口正交,且都安装面向模型的 bash 工具;sandbox 安装本地沙箱提供方和沙箱 bash 后端 |
|
||||
| `persistence` | required | `jsonl`(默认)/ `sqlite` | 每个工程恰好选择一个持久化后端 |
|
||||
| `hmr` | default | `default` | 加载 `@cordisjs/plugin-hmr`;dev 和 start 都启用,使用插件默认配置 |
|
||||
| `fs` | default | `local` | 安装本地文件系统、策略和模型工具;进程沙箱不约束进程内 fs 工具 |
|
||||
| `todo` | default | `default` | 提供 `todo_write` 工具 |
|
||||
| `skill` | default | `default` | 安装 skill(技能)注册表、本地 skill 提供方和面向模型的 skill 工具 |
|
||||
| `web` | optional | `deepseek`(默认)/ `exa` / `perplexity` / `fetch-only` | 搜索功能选项互斥;Exa/Perplexity 收集各自 API key;建议同时启用 timeout policy |
|
||||
| `subagent` | optional | `spawn`(默认)/ `fork`,可多选 | 本期只提供进程内后端 |
|
||||
| `workflow` | optional | `workerthread` | 要求 subagent 的 `spawn` 功能选项 |
|
||||
| `compact` | optional | `basic` | 使用 SDK 提供的上下文压缩参数 |
|
||||
| `hooks` | optional | `claude`(默认)/ `codex`,可多选 | 各功能选项生成独立的可编辑配置文件 |
|
||||
| `guard` | optional | `repeat-tool` | 提供重复工具调用提醒 |
|
||||
| `timeout-policy` | optional | `default` | 对声明超时预算的工具执行统一策略 |
|
||||
| `ask-user` | optional | `default` | 提供 `ask_user_question` 工具;注入的 user-interaction 服务由 acp/stdio 两个功能选项提供,因此仅这两个接口可选 |
|
||||
|
||||
`bash` 的两个功能选项都适用于 ACP、stdio 和 embed,不由运行接口决定。sandbox 功能选项不写任何生效的配置键,因而沿用 `dsh-bash-sandbox` 的 `read-only` 默认值;生成的 `cordis.yml` 保留注释示例,开发者可以显式改为 `workspace-write`:
|
||||
|
||||
```yaml
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-sandbox'
|
||||
# Uncomment to allow writes under the project workspace.
|
||||
# config:
|
||||
# mode: workspace-write
|
||||
# workspaceRoot: !!js process.cwd()
|
||||
```
|
||||
|
||||
功能贡献只引用单插件 NPM 包,绝不引用 `agent-spine-demo`、`stdio-demo`、`acp-demo` 这类组合 NPM 包。表格之外的插件不由本期 create 管理;开发者仍可直接编辑普通工程文件进行高级组合。
|
||||
|
||||
## 生成工程
|
||||
|
||||
使用默认答案创建 npm 工程时,provider 为 DeepSeek,运行接口为 stdio,bash 为 local,持久化为 JSONL,hmr、fs、todo 与 skill 处于选中状态。初始目录树为:
|
||||
|
||||
```text
|
||||
my-agent/
|
||||
├── .env
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── cordis.yml
|
||||
├── index.ts
|
||||
├── package.json
|
||||
├── tsconfig.base.json
|
||||
├── tsconfig.json
|
||||
└── tsdown.config.ts
|
||||
```
|
||||
|
||||
`.env.example` 始终存在,并由 SDK 根据当前功能维护占位。收集到 secret 或开发者确认稍后填写空凭据时,同时生成 gitignored `.env`。SDK 只向 `.env` 追加尚不存在的不同名变量,绝不覆盖或删除已有内容;切换功能选项可以清理 `.env.example` 中不再需要的占位,但旧凭据仍留在 `.env` 中供开发者自行处理。pnpm 和 Yarn 工程增加各自所需的 workspace 配置文件,但运行时插件树和 TypeScript 入口不分叉。
|
||||
|
||||
生成的 `package.json` 提供以下 scripts;其中 `dev`、`build`、`start` 与 `config` 调用 `dsh-sdk`,`typecheck` 直接调用 TypeScript:
|
||||
|
||||
| script | 行为 |
|
||||
|---|---|
|
||||
| `dev` | 运行 `dsh-sdk dev index.ts`,为 TypeScript 和本地 workspace 插件注册开发期解析 |
|
||||
| `build` | 运行 `dsh-sdk build`,调用工程安装的 tsdown 构建根入口和 `plugins/*` package |
|
||||
| `typecheck` | 直接运行 `tsc -b` |
|
||||
| `start` | 运行 `dsh-sdk start index.js`,启动已构建入口且不隐式构建 |
|
||||
| `config` | 运行 `dsh-sdk config`,修改当前工程功能树 |
|
||||
|
||||
`dsh-sdk start` 与 `dsh-sdk dev` 可以接收模块 target,并把 `--` 后的参数原样转发给工程入口。通用参数解析使用 Node `parseArgs()` 的零 schema 模式:带值 flag 采用 `--key=value`,bare flag 转换为 `true`,`--no-*` 转换为 `false`。
|
||||
|
||||
- stdio 工程通过 `--model=<name>` 传入所选 model,并根据可选的 `--resume=<session-id>` 创建或恢复 agent;
|
||||
- acp 使用协议 `session/load`
|
||||
- embed 使用生成代码中的 model。
|
||||
|
||||
每个功能拥有的 Cordis 配置项在 `cordis.yml` 中保留自己的可编辑 Cordis 插件配置和说明注释;`dsh-sdk config` 修改其他功能时必须保留未知字段、未修改节点的格式和注释。HMR(热模块替换)是普通叶子配置项:选择该功能后,dev 和 start 加载同一个 watcher,命令不隐式改变插件树。
|
||||
|
||||
## 创建后的配置
|
||||
|
||||
`dsh-sdk config` 只要求当前目录具有可读的根 `package.json` 与 `cordis.yml`。它检查标准功能及其当前功能选项,以一棵功能树表达最终目标状态,并在 Review & Apply 前展示功能变化和受影响文件。
|
||||
|
||||
`dsh-sdk config` 可以安装缺失功能、启停已安装功能和切换有限功能选项。required 功能不能取消。改变 NPM 依赖后只运行一次项目包管理器安装;安装失败不回滚已经提交的工程文件。
|
||||
|
||||
SDK 只修改功能明确拥有的 Cordis 配置项、配置键、NPM 依赖、`.env.example` 占位和独占文件。同一功能选项的更新保留 Cordis 配置项中的未知配置键;手写或第三方插件只支持按稳定 ID 启停。已知功能被手改成不完整、歧义或无法读取的形状时,`dsh-sdk config` 显示诊断并拒绝自动修改,直到开发者手工修复。
|
||||
|
||||
一次 config 会话在内存工作区上累计全部修改。Apply 前完成功能关系、资源冲突和文件形状校验,并比较受影响文件与会话打开时的原文;校验失败或检测到外部修改时不写盘。实际写盘开始后不提供跨文件事务回滚。
|
||||
|
||||
## 维护模型
|
||||
|
||||
Builtin 支持集由 SDK 人工策划,不根据 NPM 依赖名称或目录约定自动暴露。一个功能可以组合多个 Cordis 配置项,功能选项可以共享资源,并声明对其他功能或特定功能选项的功能依赖;新增普通功能或功能选项不应要求同时修改 create 和 config 两个命令流程。
|
||||
|
||||
## 后续工作
|
||||
|
||||
- `dsh-sdk add [package-spec]`:统一本地插件创建与外部 Cordis 插件接入;未指定 package 或仓库来源时创建本地 plugin/tool,指定来源时增加 NPM 依赖和 `cordis.yml` 配置项,来源模型为 GitHub 仓库等扩展保留空间
|
||||
- 非交互 create/config:本期两个流程都要求 TTY,不提供供自动化调用的完整输入合同
|
||||
- 更多功能专用参数输入:本期产品只展示有限功能选项、secret 和少量专用值,不为 Cordis 插件配置提供通用参数界面
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**不可编辑的 preset 或生成器托管工程。** 该方案可以缩短初次创建路径,但会隐藏真实插件树和构建边界,使高级开发者无法直接组合 Cordis 插件,也让项目行为依赖 CLI 版本而不是检入的工程文件。
|
||||
|
||||
**只提供一次性生成器。** 创建后完全依赖手工维护,会让功能依赖、功能选项切换和多文件更新再次分散;共享 registry 的 config 流程为生成工程保留持续管理机制。
|
||||
|
||||
**为开发和生产维护两份 `cordis.yml`。** 两份插件树会使开发成功无法证明生产加载相同功能;dev 只增加 TypeScript 与本地 workspace 解析,运行配置保持唯一。
|
||||
|
||||
**为任意 Cordis 插件配置生成通用表单。** Cordis 插件配置包含嵌套结构、表达式和插件特有语义,通用表单会形成第二套不完整 schema。SDK 只管理有限功能选项和专用 secret,复杂配置继续由开发者直接编辑。
|
||||
|
||||
**使用私有协议发现本地插件。** 普通 package manager workspace、根 NPM 依赖、TypeScript references 和 Cordis 配置项已能表达完整关系;额外发现协议会创造只能由 SDK 理解的隐藏状态。
|
||||
|
||||
**在现有工程中提供 `dsh-sdk create`。** create 已能生成一种可编辑的本地插件骨架,后续插件可以沿用普通 workspace 和 Cordis 机制手工添加;再提供同构命令会增加第二条脚手架产品面,却不增加新的组合功能。
|
||||
|
||||
**把每个新 Cordis 插件自动暴露为 builtin。** package 无法说明多个插件如何组合成一项产品功能,也无法推导互斥关系、功能依赖、secret、接口适用性和安全限制;支持集需要人工策划,自动化只适合检查候选是否完成分类。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `npm create @deepseek-ai/sdk` 按本文顺序收集项目身份、provider、interface、功能、可选本地插件、包管理器和安装选择,并在取消时保持目标路径不存在
|
||||
- 默认 npm 工程具有本文目录树和 `dev`、`build`、`typecheck`、`start`、`config` scripts,且 dev/start 使用同一份 `cordis.yml`
|
||||
- create 展示本文功能及功能选项;`bash` 的 local/sandbox 二选一且默认 local,sandbox Cordis 配置项保留可编辑的注释配置示例;HMR 默认选中并同时由 dev/start 加载
|
||||
- create 的 `plugin` 或 `tool` 选择至多生成一个固定名称的本地插件,并原子更新插件文件与根工程关系;本期不提供 `dsh-sdk create`
|
||||
- `dsh-sdk config` 从现有工程读取同一支持集,能够安装、启停和切换支持的功能选项,保留未知配置与注释,并拒绝修改不一致配置
|
||||
- `.env.example` 反映当前功能所需变量;`.env` 只追加缺失的不同名变量,从不覆盖或清理已有内容
|
||||
- npm、pnpm 和 Yarn 生成的 workspace 能安装、构建和启动;本地插件在 dev 中使用源码,在 start 中使用构建产物
|
||||
|
||||
## 风险
|
||||
|
||||
- 开发者可以把 builtin 手改成 registry 无法识别的形状;SDK 选择停止自动化而不是猜测并覆盖配置
|
||||
- 多文件写入前的校验和外部修改检测不能提供写入阶段的事务回滚;I/O 中途失败可能留下需要人工修复的部分提交
|
||||
- sandbox 功能选项依赖目标平台存在可用的本地沙箱后端;后端不可用时必须 fail closed,不能退回无沙箱执行
|
||||
- HMR 在生产启动中也保持文件 watcher 和热重载行为;这是显式插件选择的结果,不是仅限开发环境的隐式服务
|
||||
- `.env` 的仅追加策略会保留已经不用的凭据,SDK 不判断这些用户数据何时可以安全删除
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-17-sdk-follow-up-capabilities.md: 0f3ada6bdbb4ce933d14602cf59be9a51640e61c
|
||||
2026-07-17-sdk-follow-up-capabilities.zh.md: d0d0b3e6bcdf192e64f003dc9f6e90cc2bdb060b
|
||||
@@ -0,0 +1,118 @@
|
||||
# Agent Note: SDK follow-up capabilities
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-17-sdk-follow-up-capabilities.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The first SDK release creates and edits developer-owned Cordis projects through the shared model defined by the [developer-project Agent Note](2026-07-14-sdk-developer-projects.md) and the [project-editing architecture](../architecture/2026-07-15-sdk-project-editing-architecture.md). Its create and config workflows are interactive, external Cordis plugins require manual dependency and configuration edits, command-line telemetry has no owning boundary, and interactive branches lack a stable test strategy.
|
||||
|
||||
These gaps are coupled. Create and config already share questions, feature configuration, and `ProjectEditSession`; adding separate automation paths would duplicate that domain logic. External-plugin installation must update both the package manager's files and `cordis.yml`. Telemetry must observe commands such as create and build that do not boot Cordis. Interactive testing must exercise Harness behavior without making terminal rendering a brittle product contract.
|
||||
|
||||
## Proposal
|
||||
|
||||
The SDK extends the existing prompt and project-editing boundaries instead of creating parallel workflows. A non-interactive prompt port and structured feature plan drive create and config, `dsh-sdk create <source>` delegates dependency resolution to the project package manager before mounting the resolved package through `ProjectEditSession`, launcher-side telemetry wraps `create-sdk` and every `dsh-sdk` command, and injected prompt streams provide the primary interactive-test seam.
|
||||
|
||||
| Capability | Product entrypoint | Owning mechanism | Required outcome |
|
||||
|---|---|---|---|
|
||||
| Headless project creation | `create-sdk --config <file>` or `--config-json <json>` with optional `--json` | `HeadlessPromptPort`, structured project answers, and a complete feature plan | No terminal blocking; missing required input is explicit |
|
||||
| External Cordis plugin installation | `dsh-sdk create <source>` | Native package-manager `add` plus `ProjectEditSession` | The dependency and `cordis.yml` entry identify the package manager's resolved package |
|
||||
| Developer-cycle telemetry | `create-sdk` and every `dsh-sdk` command | Launcher-side consent, payload, redaction, anonymous identity, and delivery services | Reporting is best-effort and cannot change the command result |
|
||||
| Interactive regression coverage | Create and config tests | Injected `PromptPort` input/output and filesystem assertions | Tests cover Harness decisions and generated files without snapshotting terminal repainting |
|
||||
|
||||
## Shared headless workflow
|
||||
|
||||
### Structured input and lifecycle events
|
||||
|
||||
Headless create accepts a JSON object either inline through `--config-json` or from a file through `--config`. Scalar fields supply the ordinary create answers, while `features` supplies the complete selected feature set, feature options, secrets, and dedicated values. Defaults remain valid only where the owning question declares one; the headless path never invents an answer for a required prompt.
|
||||
|
||||
With `--json`, stdout is an NDJSON event stream. `done` means creation and any requested setup completed, `action-required` names an unanswered required prompt, and `error` reports another failure. Human-readable progress and package-manager output go to stderr so every stdout line remains parseable as one event. A caller responds to `action-required` by adding the missing value and running the command again.
|
||||
|
||||
Create and config consume the same feature-plan shape. Create exposes it through the command-line inputs above; config uses it at the shared workflow boundary so a later automation entrypoint does not need a second feature-selection model.
|
||||
|
||||
### Prompt and project-editing boundaries
|
||||
|
||||
`PromptPort` remains the only boundary between SDK questions and an interaction implementation. `ClackPromptPort` handles terminals. `HeadlessPromptPort` consumes defaults exposed by the question contract and otherwise fails with the unanswered prompt; prefilled values normally prevent the port from being called.
|
||||
|
||||
Both paths use the same `Question` objects, `FeatureConfigurator`, `SdkProject`, and `ProjectEditSession`. The headless path therefore changes how answers arrive, not how features are interpreted or files are committed.
|
||||
|
||||
### Agent skill
|
||||
|
||||
The repository ships a thin `SKILL.md` that teaches an agent to construct the structured input, request NDJSON, fill an `action-required` value, and retry. The skill invokes the public CLI and does not import an internal SDK API or introduce another project specification.
|
||||
|
||||
## External Cordis plugin installation
|
||||
|
||||
`dsh-sdk create <source>` accepts a package-manager-native npm specifier such as `pkg@version` or a GitHub specifier such as `github:owner/repo#ref`. After confirmation, it asks the project's package manager to add the source, compares the direct dependency names before and after the operation, reopens the project, and mounts each newly resolved package in `cordis.yml` through `ProjectEditSession`.
|
||||
|
||||
The package manager owns source parsing, version or commit resolution, integrity data, lockfile updates, and any build policy. The SDK does not download or unpack a second copy through giget or pacote. An external plugin remains a dependency under `node_modules`; local plugin scaffolding remains a separate project-creation concern.
|
||||
|
||||
## Launcher telemetry
|
||||
|
||||
### Consent and collection
|
||||
|
||||
Telemetry wraps the `create-sdk` initializer and the `dsh-sdk` launcher command lifecycle because project initialization, plugin creation, and build do not reliably boot Cordis. One event records the command name, duration, success, a random per-user anonymous identifier, and redacted `cordis.yml` and `package.json` text when those project files are eligible.
|
||||
|
||||
Reporting is enabled unless a present telemetry config entry is explicitly disabled. `DO_NOT_TRACK` and CI deny reporting regardless of project configuration. A missing `cordis.yml` does not itself deny the event, but `package.json` content is included only when `cordis.yml` establishes that the directory is an SDK project.
|
||||
|
||||
### Safety and delivery
|
||||
|
||||
The payload builder never reads `.env`. It redacts secret-shaped keys and values, known token forms, PEM blocks, URL credentials, and high-entropy opaque strings in the two eligible text files. Redaction is a safety backstop rather than a guarantee; SDK projects must keep credentials in `.env`.
|
||||
|
||||
The reporter uses a fixed endpoint and resolves every send path without throwing. Command dispatch records success or failure in a `finally` path, starts reporting after the command outcome is known, and drains within a bounded interval. Consent parsing, payload construction, storage, or network failures are swallowed only at this telemetry boundary and never alter the command's exit code.
|
||||
|
||||
## Interactive workflow testing
|
||||
|
||||
Create and config tests inject a `PromptPort` and scripted input/output streams into the existing workflows. Parameterized scenarios cover feature selection, feature options, secrets, cancellation, review, and apply behavior, then assert the resulting `cordis.yml` and other project files. The stable product assertion is the generated project state, not clack's ANSI redraw sequence.
|
||||
|
||||
One or two optional real-PTY smoke tests may cover the shipped binary and TTY guard that injection cannot reproduce. Native PTY tooling does not belong on the required path unless it is reliable across the repository's supported Node and host versions.
|
||||
|
||||
## Deferred work
|
||||
|
||||
- Extend the headless create specification to express local `plugin` or `tool` scaffolding instead of defaulting that interactive choice to none.
|
||||
- Expose the telemetry opt-out in create and config while preserving the consent representation in which only a disabled telemetry entry is written.
|
||||
- Define whether GitHub source dependencies must be prebuilt or may run package-manager-controlled preparation scripts, and surface the policy before installation.
|
||||
- Replace the telemetry package's `.invalid` endpoint placeholder with the production endpoint before release.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Build a separate headless creation engine.** This would duplicate questions, feature requirements, configuration behavior, and project-editing rules. Reusing the prompt and edit-session boundaries keeps one implementation of project semantics.
|
||||
|
||||
**Make a specification file the primary automation interface.** Agents can pass the same typed JSON object inline, while people and CI may still use a file. A file-only protocol adds persistence and cleanup without adding semantics.
|
||||
|
||||
**Use `npx skills add` as the project creator.** The skills CLI installs Markdown skills; it does not create SDK projects or install npm packages. The agent skill therefore drives the SDK initializer instead of replacing it.
|
||||
|
||||
**Fetch GitHub and npm sources through giget or pacote.** A second fetch layer would duplicate package-manager resolution, integrity, lockfile, and lifecycle policy. Native dependency specifiers keep those decisions in the selected package manager.
|
||||
|
||||
**Implement telemetry as a Cordis runtime plugin.** Create and build do not necessarily boot Cordis, so a runtime plugin cannot observe the complete developer command cycle. The launcher is the boundary shared by those commands.
|
||||
|
||||
**Derive the anonymous identifier from git metadata.** Repository remotes can identify a project or organization. A random per-user identifier supports aggregation without encoding repository identity.
|
||||
|
||||
**Collect only aggregate counters.** Aggregate-only events reduce exposure but cannot answer which plugins, dependencies, and configuration shapes developers actually use. This proposal accepts collection of redacted project text and makes that exposure explicit.
|
||||
|
||||
**Use real PTYs and transcript snapshots as the primary test strategy.** Native PTY dependencies and terminal repaint sequences add platform and rendering instability while mostly testing clack. Injected interaction plus generated-file assertions tests the SDK-owned behavior directly.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Create runs without a TTY from a complete structured input, emits only NDJSON on stdout under `--json`, and reports missing required input as `action-required` without writing a partial project.
|
||||
- Create and config resolve the same feature-plan contract through the shared question, feature-configuration, and project-editing code paths.
|
||||
- `dsh-sdk create <source>` uses the selected project package manager, mounts the dependency name that operation actually added, and fails loudly when no new dependency can be identified.
|
||||
- The initializer and every `dsh-sdk` command reach one best-effort telemetry completion path; an explicit disabled entry, `DO_NOT_TRACK`, or CI prevents delivery, and telemetry failures never change the command result.
|
||||
- Telemetry never reads `.env`, withholds unrelated `package.json` content when no `cordis.yml` exists, redacts both eligible text payloads, and uses an identifier unrelated to git metadata.
|
||||
- Interactive tests cover create and config decisions through injected interaction and assert committed project files; any real-PTY coverage remains a narrow smoke layer.
|
||||
- The agent skill documents the public structured-input and event contracts without depending on private package exports.
|
||||
|
||||
## Risks
|
||||
|
||||
- Full redacted `cordis.yml` and `package.json` text still reveals plugin and dependency names, URLs, paths, and configuration values to the endpoint operator, and heuristic redaction can miss a secret.
|
||||
- Default-on reporting may surprise developers when no telemetry entry exists; the CLI must make the opt-out discoverable before release.
|
||||
- A package-manager add can change `package.json`, the lockfile, and installed files before `ProjectEditSession` mounts the plugin, so a later mount failure can leave dependency changes that require manual recovery.
|
||||
- GitHub dependencies may execute preparation or lifecycle code according to package-manager policy; an unresolved build policy is a supply-chain and reproducibility risk.
|
||||
- Injected prompt tests do not prove raw-mode, signal, or repaint behavior in a real terminal; the optional smoke layer must cover only those residual contracts.
|
||||
|
||||
## References
|
||||
|
||||
- [Vercel Eve](https://github.com/vercel/eve) and [Vercel Labs Skills](https://github.com/vercel-labs/skills) for the distinction between a headless initializer and skill distribution.
|
||||
- [npm package specifications](https://docs.npmjs.com/cli/v11/using-npm/package-spec), [pnpm add](https://pnpm.io/cli/add), and [Yarn add](https://yarnpkg.com/cli/add) for package-manager-native sources.
|
||||
- [`DO_NOT_TRACK`](https://donottrack.sh/) for the environment-level opt-out convention.
|
||||
- [Clack](https://github.com/bombshell-dev/clack) and [Vitest snapshots](https://vitest.dev/guide/snapshot) for injected prompts and generated-file assertions.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Agent Note: SDK 后续功能
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-17-sdk-follow-up-capabilities.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
首个 SDK 版本通过[开发者工程 Agent Note](2026-07-14-sdk-developer-projects.md) 和 [SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md)定义的共享模型创建和编辑开发者拥有的 Cordis 工程。create 和 config 工作流仅支持交互调用,接入外部 Cordis 插件需要手工修改依赖和配置,命令行遥测没有明确的所属边界,交互分支也缺少稳定的测试策略。
|
||||
|
||||
这些缺口彼此关联。create 和 config 已经共享问题、功能配置和 `ProjectEditSession`;若另建自动化路径,就会复制领域逻辑。安装外部插件必须同时修改包管理器文件和 `cordis.yml`。遥测需要观察 create、build 等不会启动 Cordis 的命令。交互测试需要覆盖 Harness 自身行为,同时避免把终端渲染固化成脆弱的产品契约。
|
||||
|
||||
## 提案
|
||||
|
||||
SDK 扩展现有提示词与工程编辑边界,不另建平行工作流。非交互式 `PromptPort` 实现和结构化功能计划驱动 create 与 config;`dsh-sdk create <source>` 先把依赖解析交给工程的包管理器,再通过 `ProjectEditSession` 挂载解析所得的包;启动器侧遥测包住 `create-sdk` 和每个 `dsh-sdk` 命令;交互测试主要通过注入的提示词输入输出流完成。
|
||||
|
||||
| 功能 | 产品入口 | 所属机制 | 必须达到的结果 |
|
||||
|---|---|---|---|
|
||||
| Headless 工程创建 | `create-sdk --config <file>` 或 `--config-json <json>`,可搭配 `--json` | `HeadlessPromptPort`、结构化工程答案和完整功能计划 | 不阻塞等待终端;明确报告缺失的必答输入 |
|
||||
| 外部 Cordis 插件安装 | `dsh-sdk create <source>` | 包管理器原生 `add` 加 `ProjectEditSession` | 依赖和 `cordis.yml` 配置项指向包管理器解析出的包 |
|
||||
| 开发周期遥测 | `create-sdk` 和每个 `dsh-sdk` 命令 | 启动器侧的上报条件判断、遥测内容构建、脱敏、匿名身份和传输服务 | 上报采用尽力而为语义,不能改变命令结果 |
|
||||
| 交互回归覆盖 | create 和 config 测试 | 注入的 `PromptPort` 输入输出和文件系统断言 | 测试覆盖 Harness 决策与生成文件,不快照终端重绘 |
|
||||
|
||||
## 共享 headless 工作流
|
||||
|
||||
### 结构化输入和生命周期事件
|
||||
|
||||
Headless create 通过 `--config-json` 接收内联 JSON 对象,或通过 `--config` 从文件读取。标量字段提供普通 create 答案,`features` 提供完整的已选功能、功能选项、secret(密钥)和专用值。只有所属问题明确声明的默认值才有效;headless 路径绝不为必答问题臆造答案。
|
||||
|
||||
使用 `--json` 时,stdout 是 NDJSON 事件流。`done` 表示创建及要求执行的安装和构建均已完成,`action-required` 指明一个尚未回答的必答问题,`error` 报告其他失败。面向人的进度信息和包管理器输出写入 stderr,确保 stdout 每一行都能解析成一个事件。调用方收到 `action-required` 后补充缺失值,再次运行命令。
|
||||
|
||||
Create 和 config 使用相同的功能计划形状。create 通过上述命令行输入公开该形状;config 在共享工作流边界使用同一形状,使后续自动化入口无需另建功能选择模型。
|
||||
|
||||
### Prompt 与工程编辑边界
|
||||
|
||||
`PromptPort` 仍是 SDK 问题与交互实现之间的唯一边界。`ClackPromptPort` 负责终端交互。`HeadlessPromptPort` 使用问题契约公开的默认值,否则通过未回答问题快速失败;预填值通常会让流程根本不调用该 port。
|
||||
|
||||
两条路径使用相同的 `Question` 对象、`FeatureConfigurator`、`SdkProject` 和 `ProjectEditSession`。因此,headless 路径只改变答案的到达方式,不改变功能解释或文件提交方式。
|
||||
|
||||
### Agent skill
|
||||
|
||||
仓库提供一份轻量 `SKILL.md`,指导 agent skill(智能体技能)构造结构化输入、请求 NDJSON、补充 `action-required` 指明的值并重试。该 skill 调用公开 CLI,不导入 SDK 内部 API,也不引入另一套工程规格。
|
||||
|
||||
## 外部 Cordis 插件安装
|
||||
|
||||
`dsh-sdk create <source>` 接受包管理器原生的 npm package specifier,例如 `pkg@version`,也接受 `github:owner/repo#ref` 等 GitHub package specifier。用户确认后,命令要求工程包管理器添加来源,对比操作前后的直接依赖名,重新打开工程,再通过 `ProjectEditSession` 把每个新增且已解析的包挂载进 `cordis.yml`。
|
||||
|
||||
包管理器负责来源解析、版本或 commit 解析、`integrity` 数据、lockfile 更新和构建策略。SDK 不再通过 giget 或 pacote 下载、解压第二份副本。外部插件是 `node_modules` 下的依赖;本地插件脚手架仍属于独立的工程创建问题。
|
||||
|
||||
## Launcher 遥测
|
||||
|
||||
### Consent 与采集
|
||||
|
||||
遥测包住 `create-sdk` 初始化命令与 `dsh-sdk` launcher 的命令生命周期,因为工程初始化、插件创建和 build 都不会稳定地启动 Cordis。每个事件记录命令名、时长、成败、随机生成的用户级匿名标识符,以及符合条件时经过脱敏的 `cordis.yml` 与 `package.json` 文本。
|
||||
|
||||
除非当前存在的遥测配置项被明确禁用,否则允许上报。`DO_NOT_TRACK` 和 CI 无论工程配置如何都禁止上报。缺少 `cordis.yml` 本身不会禁止事件,但只有 `cordis.yml` 能证明目录是 SDK 工程时,遥测内容才包含 `package.json` 文本。
|
||||
|
||||
### 安全与传输
|
||||
|
||||
Payload 构建器绝不读取 `.env`。它会脱敏两个符合条件的文本文件中的疑似密钥键和值、已知 token 形式、PEM 块、URL 凭据和高熵不透明字符串。脱敏只是安全兜底,不能提供绝对保证;SDK 工程必须把凭据放进 `.env`。
|
||||
|
||||
`TelemetryReporter` 使用固定 endpoint,每条发送路径都会正常结束且不抛错。命令分发通过 `finally` 路径记录成败,在命令结果已确定后启动上报,并在有界时间内等待传输结束。只有遥测边界会吞掉上报条件解析、遥测内容构建、存储或网络错误,这些错误绝不改变命令退出码。
|
||||
|
||||
## 交互工作流测试
|
||||
|
||||
Create 和 config 测试向现有工作流注入 `PromptPort` 和脚本化输入输出流。参数化场景覆盖功能选择、功能选项、secret、取消、评审和应用行为,再断言最终的 `cordis.yml` 及其他工程文件。稳定的产品断言是生成后的工程状态,不是 clack 的 ANSI 重绘序列。
|
||||
|
||||
可以用一到两个可选的真实 PTY 冒烟测试覆盖注入无法复现的发布二进制和 TTY 检查。除非原生 PTY 工具在仓库支持的 Node 与宿主版本上足够可靠,否则它不进入必跑路径。
|
||||
|
||||
## 延后工作
|
||||
|
||||
- 扩展 headless create 规格,使其能表达本地 `plugin` 或 `tool` 脚手架,而不是把该交互选择默认为 none。
|
||||
- 在 create 和 config 中公开遥测关闭选项,同时保留只有禁用时才写入遥测配置项的上报许可表示。
|
||||
- 明确 GitHub 来源依赖必须预先构建,还是允许运行由包管理器控制的 preparation script(准备脚本),并在安装前向用户展示该策略。
|
||||
- 发布前把遥测包中的 `.invalid` endpoint 占位符替换为生产端点。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**另建 headless 创建引擎。** 该方案会复制问题、功能依赖、配置行为和工程编辑规则。复用提示词与编辑会话边界,可以保证工程语义只有一份实现。
|
||||
|
||||
**把规格文件作为主要自动化接口。** Agent 可以内联传入相同的类型化 JSON 对象,人和 CI 仍可选用文件。文件专用协议会增加持久化与清理工作,却不增加语义。
|
||||
|
||||
**使用 `npx skills add` 创建工程。** Skills CLI 只安装 Markdown skill,不创建 SDK 工程,也不安装 npm 包。因此,agent skill 驱动 SDK 初始化命令,而不是取代它。
|
||||
|
||||
**通过 giget 或 pacote 获取 GitHub 与 npm 来源。** 第二套获取层会复制包管理器的解析、完整性、lockfile 和生命周期策略。原生 package specifier 让这些决策留在所选包管理器中。
|
||||
|
||||
**把遥测实现成 Cordis 运行时插件。** Create 和 build 不一定启动 Cordis,因此运行时插件无法观察完整的开发命令周期。Launcher 是这些命令共用的边界。
|
||||
|
||||
**从 git 元数据派生匿名标识符。** 仓库的 git remote 可能识别工程或组织。随机的用户级标识符能够支持聚合,同时不编码仓库身份。
|
||||
|
||||
**只采集聚合计数。** 仅聚合事件可以降低暴露,但无法回答开发者实际使用哪些插件、依赖和配置形状。本提案接受采集脱敏后的工程文本,并明确记录这项暴露。
|
||||
|
||||
**把真实 PTY 和 transcript(文本记录)快照作为主要测试策略。** 原生 PTY 依赖与终端重绘序列会带来平台和渲染不稳定性,而且主要是在测试 clack。注入交互并断言生成文件,可以直接测试 SDK 拥有的行为。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- Create 能依据完整结构化输入在没有 TTY 时运行;使用 `--json` 时 stdout 只输出 NDJSON;缺少必答输入时通过 `action-required` 报告,且不写入部分工程。
|
||||
- Create 和 config 通过共享的问题、功能配置和工程编辑代码路径解析相同的功能计划契约。
|
||||
- `dsh-sdk create <source>` 使用工程选定的包管理器,挂载该操作实际新增的依赖名;无法识别新增依赖时快速失败。
|
||||
- 初始化命令与每个 `dsh-sdk` 命令都进入同一条尽力而为的遥测收尾路径;明确禁用的配置项、`DO_NOT_TRACK` 或 CI 会阻止传输,遥测失败绝不改变命令结果。
|
||||
- 遥测绝不读取 `.env`;没有 `cordis.yml` 时不发送无关的 `package.json` 内容;两个符合条件的文本都经过脱敏;匿名标识符与 git 元数据无关。
|
||||
- 交互测试通过注入交互覆盖 create 和 config 决策,并断言已提交的工程文件;真实 PTY 覆盖只作为窄范围冒烟层。
|
||||
- Agent skill 说明公开的结构化输入与事件契约,不依赖包的私有导出。
|
||||
|
||||
## 风险
|
||||
|
||||
- 即使经过脱敏,完整的 `cordis.yml` 与 `package.json` 文本仍会向 endpoint 运营方暴露插件名、依赖名、URL、路径和配置值;启发式脱敏也可能漏掉 secret。
|
||||
- 没有遥测配置项时默认上报可能让开发者意外;发布前 CLI 必须让关闭方法易于发现。
|
||||
- 在 `ProjectEditSession` 挂载插件前,包管理器的 add 操作已经可能修改 `package.json`、lockfile 和安装文件;后续挂载失败会留下需要手工恢复的依赖改动。
|
||||
- GitHub 依赖可能按包管理器策略执行 preparation 或 lifecycle script;尚未解决的构建策略会带来供应链与可复现性风险。
|
||||
- 注入提示词交互的测试无法证明真实终端中的 raw mode、signal 或重绘行为;可选冒烟层只应覆盖这些残余契约。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Vercel Eve](https://github.com/vercel/eve) 与 [Vercel Labs Skills](https://github.com/vercel-labs/skills) 用于区分 headless 初始化命令与 skill 分发。
|
||||
- [npm package specifications](https://docs.npmjs.com/cli/v11/using-npm/package-spec)、[pnpm add](https://pnpm.io/cli/add)和 [Yarn add](https://yarnpkg.com/cli/add)说明包管理器原生来源。
|
||||
- [`DO_NOT_TRACK`](https://donottrack.sh/)定义环境级关闭约定。
|
||||
- [Clack](https://github.com/bombshell-dev/clack) 和 [Vitest snapshots](https://vitest.dev/guide/snapshot) 说明注入提示词交互与生成文件断言。
|
||||
Reference in New Issue
Block a user