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

# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/sandbox-acp-agent/cordis.yml
#	examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl
#	packages/compact/compact-basic/README.md
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/tests/properties.spec.ts
#	packages/core/session/README.md
#	packages/core/session/src/types.ts
#	packages/core/session/tests/derived-cache.spec.ts
#	packages/llm/llm-deepseek/src/index.ts
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm-pi-ai/src/convert.ts
#	packages/llm/llm-pi-ai/tests/adapter.spec.ts
#	packages/llm/llm/README.md
#	packages/llm/llm/src/call-config.ts
#	packages/llm/llm/src/index.ts
#	packages/ui/acp-agent/src/index.ts
#	packages/ui/acp/tests/harness.ts
#	packages/ui/jsonrpc/README.md
#	packages/ui/jsonrpc/src/server.ts
#	packages/ui/stdio-agent/README.md
#	packages/ui/stdio-agent/src/index.ts
#	python/sdk/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-07-14 22:17:50 +08:00
672 changed files with 10295 additions and 14200 deletions
+4 -17
View File
@@ -52,14 +52,8 @@ export class DeepSeekAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const body = serializeRequest(options, this.options.defaults ?? {})
// TODO(http): deliberately raw `fetch` for the hand-rolled SSE body.
// `@cordisjs/plugin-http` (ctx.http) would give proxy/intercept/timeout
// uniformity AND can stream (`responseType: 'stream'` yields the same
// ReadableStream<Uint8Array> parseSse consumes), but adopting it today
// costs a hard `undici` dependency (it does `require('undici')` with no
// globalThis.fetch fallback) plus an unconditional `@cordisjs/fetch-file`
// import (pulling file-type + mime-types) for a file:// path we never hit.
// Revisit when a second adapter wants shared proxy/intercept config.
// TODO(http): adopt the Cordis HTTP service when shared transport configuration
// outweighs its additional runtime dependencies.
const response = await fetch(`${this.options.baseURL}/chat/completions`, {
method: 'POST',
headers: {
@@ -79,15 +73,8 @@ export class DeepSeekAdapter extends LlmAdapter {
const parsed = await response.json() as WireError
if (parsed.error?.message) message = parsed.error.message
} catch {
// Paranoid by design: `code` and the HTTP status are ALREADY captured
// above (and passed to LlmError below), so the only thing this `try`
// can add is a richer provider-supplied message. A malformed, empty,
// or non-JSON error body is a normal thing for gateways/proxies to
// return on a 5xx/429 — swallowing the parse failure keeps the usable
// status-line message instead of letting a JSON.parse throw mask the
// real HTTP error. Nothing else reaches this catch: response.json()
// is the sole statement, and any non-parse failure (e.g. body already
// consumed) is equally non-actionable here.
// Only swallow error-body parsing: status and code are already captured,
// so malformed gateway JSON must not mask the actionable HTTP failure.
}
throw new LlmError(message, code, response.status)
}
+3 -15
View File
@@ -1,19 +1,7 @@
/**
* DeepSeek LLM adapter plugin: registers a {@link DeepSeekAdapter} for the
* `deepseek` provider route on `ctx.llm`.
*
* Config is cordis-native (schemastery). Secrets flow per the repo policy:
* `apiKey` from cordis.yml via the `!!js` tag (`!!js process.env.DEEPSEEK_API_KEY`)
* or from the environment directly; never from ad-hoc files.
*
* ```yaml
* - id: llm-deepseek
* name: '@deepseek-ai/dsh-llm-deepseek'
* config:
* apiKey: !!js process.env.DEEPSEEK_API_KEY
* baseURL: !!js process.env.DEEPSEEK_BASE_URL
* ```
*
* Register a {@link DeepSeekAdapter} for the `deepseek` provider route on `ctx.llm`. Configuration uses
* Cordis schemastery; pass secrets from environment variables through `cordis.yml` with `!!js`,
* as shown in the package README, rather than reading ad hoc files.
* @module @deepseek-ai/dsh-llm-deepseek
*/
+4 -13
View File
@@ -1,17 +1,8 @@
/**
* Serialize harness vocabulary (`GenerateOptions`, `Message[]`) into the
* DeepSeek chat-completions request body.
*
* Block-type mapping (core types handled explicitly; merge-extensible unions
* mean plugin-added block types exist — they are skipped, never errors):
*
* - user `text` → string content (joined)
* - assistant `text` → `content`; `reasoning` → `reasoning_content`, but
* ONLY on assistant messages that carry tool calls (the official passback
* rule for thinking mode — required there, ignored elsewhere, so we save
* the tokens elsewhere); `tool-call` → `tool_calls[]`
* - `tool-result` → its own `{role: 'tool'}` message (text flattened)
*
* Serialize harness messages into DeepSeek chat completions. User text is joined; assistant text
* becomes `content`, tool calls become `tool_calls`, and tool results become separate tool messages.
* Assistant reasoning is replayed as `reasoning_content` only on tool-call turns, as required by
* thinking-mode passback. Unknown declaration-merged block types are skipped rather than rejected.
* @module dsh-llm-deepseek/serialize
*/
+4 -10
View File
@@ -1,15 +1,9 @@
/**
* Decode an SSE byte stream into event `data` payloads. Network reads may split UTF-8 or lines;
* CRLF, comments, non-data fields, and multi-data events are handled per SSE rules. The literal
* `[DONE]` is yielded so the caller owns final flushing, and EOF before it raises {@link LlmError}.
*
* Minimal SSE (text/event-stream) parser for the chat-completions stream.
*
* Yields each event's `data:` payload as a string, ending with the literal
* `'[DONE]'` sentinel so the consumer owns end-of-stream flushing. A stream
* that closes WITHOUT `[DONE]` is a protocol violation → `LlmError`.
*
* Handles the wire realities: payloads split across network reads at
* arbitrary byte positions (including mid-UTF-8), CRLF line endings,
* multi-`data:` events (joined with newlines per the SSE spec), comment
* lines, and non-data fields (ignored).
*
* @module dsh-llm-deepseek/sse
*/
+5 -11
View File
@@ -1,16 +1,10 @@
/**
* Translate DeepSeek SSE payloads with one stateful harness block per content, reasoning, or tool
* call index. An empty initial reasoning delta does not open a block. Finish reason and the latest
* usage are deferred until `[DONE]`, covering both finish-attached and trailing usage-only shapes
* while ensuring no chunk follows `finish`.
*
* Translate DeepSeek wire chunks into the harness `StreamChunk` protocol.
*
* A small state machine over the SSE payload stream:
* - `delta.content` / `delta.reasoning_content` / `delta.tool_calls[i]` each
* own one harness block (index allocated on first sight). The first
* thinking-mode chunk carries `reasoning_content: ""` — that must NOT open
* a reasoning block.
* - `finish_reason` and `usage` are DEFERRED: emitted only at the `[DONE]`
* sentinel, so the wire's two usage shapes (attached to the finish chunk,
* or a trailing usage-only chunk) both work and nothing ever follows
* `finish`. Last usage wins.
*
* @module dsh-llm-deepseek/translate
*/