From 0236a123242f8676fe754450cde6c17ff321dde4 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:54:30 +0800 Subject: [PATCH 01/21] refactor: prune core tool and prompt surface --- docs/config-catalog.md | 2 +- docs/cordis-catalog/services.md | 2 +- docs/core-data-structures/tools.md | 3 +- .../2026-06-18-session-surface.md | 2 +- .../2026-07-07-tool-call-timeout-policy.md | 3 +- ...-12-simplify-session-log-representation.md | 2 +- .../cordis/tool-cordis/src/api-catalog.ts | 2 +- packages/core/agent-loop/src/loop.ts | 8 +--- .../agent-loop/tests/review-fixes.spec.ts | 6 +-- packages/core/session/README.md | 2 +- packages/core/session/src/surface.ts | 31 ++++------------ .../core/session/tests/derived-cache.spec.ts | 15 +------- packages/core/session/tests/surface.spec.ts | 15 +------- packages/core/system-prompt/src/index.ts | 2 +- packages/core/tools/README.md | 2 +- packages/core/tools/src/index.ts | 20 +++------- packages/core/tools/tests/scoped.spec.ts | 4 +- packages/core/tools/tests/tools.spec.ts | 37 ++++--------------- .../invariants/tests/invariants.spec.ts | 6 +-- packages/timeout/timeout-policy/src/index.ts | 7 +--- .../tests/timeout-policy.spec.ts | 18 ++------- 21 files changed, 50 insertions(+), 139 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9d4b8038b5..f785fdf337 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -994,7 +994,7 @@ export interface Config { export type ToolPresentationMode = 'native' | 'code' | 'both' ``` -Source: [`packages/core/tools/src/index.ts:401`](../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:400`](../packages/core/tools/src/index.ts) ## `@deepseek-ai/dsh-user-approval` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 0aea3f1589..28d1904ecf 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -276,7 +276,7 @@ async execute(exec: ToolExecutionInput): Promise Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) -Source: [`packages/core/tools/src/index.ts:493`](../../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:492`](../../packages/core/tools/src/index.ts) ## `ctx.userInteraction` — `UserInteractionService` diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index d7e0464f01..36c59aea17 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -137,7 +137,6 @@ type ToolGuard = (execution: Readonly) => string | undefined ```ts type-equiv interface ToolExecutionResult { - callId: CallId content: ContentBlock[] isError: boolean /** @@ -167,6 +166,8 @@ interface ToolExecutionResult { } ``` +The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. + The registry materializes and freezes the final accepted result immediately before `tools/result`. Its content, structured error, additional context, and presentation metadata must round-trip losslessly through JSON; an invalid outcome becomes a JSON-safe `isError` result, so the observed live outcome is safe for the later durable `tool/result` append. Each interception waterfall returns a typed **Decision** (the idiom shared with the `agent/*` seams). `tools/pre-execute` listeners receive `(exec, next)` and return a `PreToolDecision`; `tools/execute` wrappers return a `ToolExecutionResult`; `tools/post-execute` listeners receive `(exec, result, next)` and return a `PostToolDecision`: diff --git a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md b/docs/rfc/implemented/architecture/2026-06-18-session-surface.md index 4c8b5a81e3..315771e46e 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md +++ b/docs/rfc/implemented/architecture/2026-06-18-session-surface.md @@ -31,7 +31,7 @@ export type SurfaceOp = ### SurfaceManager: delta-based, not full rebuild -A `SurfaceManager` class (private to `Session`) maintains the cached linked list. It tracks `_lastProcessedSeq` and processes only the **delta** (new events since the last access) rather than rescanning the entire log. Because the log is append-only, prior events never change — full rebuild is only needed after a wholesale log replacement (e.g., seeding). +A `SurfaceManager` owned by `Session` maintains the cached linked list. It tracks `_lastProcessedSeq` and processes only the **delta** (new events since the last access) rather than rescanning the entire log. The seed is fixed before the manager is created and the log is append-only afterward, so prior events never change and no invalidation path is needed. Delta processing is O(1) when no new events and O(new events) when new events arrive. diff --git a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md index 362f5cb5e8..ab6be2efc3 100644 --- a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md +++ b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md @@ -57,9 +57,8 @@ Signal replacement is by **in-place mutation of `exec.signal`**, not by passing `timeout-policy` owns both uses of the `TOOL_TIMEOUT` code: the internal deadline code passed to `deadline()`/`timeoutOf()` (scoped so a nested outer deadline reads as an ordinary cancel) and the structured tool-result error code. Its replacement result is: ```ts ignore-check -function toolTimeoutResult(callId: CallId, timeoutMs: number): ToolExecutionResult { +function toolTimeoutResult(timeoutMs: number): ToolExecutionResult { return { - callId, content: [{ type: 'text', text: `Error: tool call timed out after ${timeoutMs}ms` }], isError: true, error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, diff --git a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md index f335afafe1..715ce93924 100644 --- a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md +++ b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md @@ -26,7 +26,7 @@ Amend the session-surface and reconstructable-request RFCs where they describe t ## Acceptance criteria -- `SurfaceManager.nodes` is one ordered seq array with no `SurfaceNode`, link fields, or seq-to-node map; incremental append processing and the internal replace-generation signal remain, while the separate public `invalidate()` deletion stays owned by the dead-surface RFC. +- `SurfaceManager.nodes` is one ordered seq array with no `SurfaceNode`, link fields, or seq-to-node map; incremental append processing and the internal replace-generation signal remain. - Replaying full changed-header snapshots reconstructs exactly the same requests; no header-delta event/type/codec remains. - A v0 seed or persisted log containing legacy `request/header-delta` is rejected before replay, with coverage for JSONL and SQLite load paths. - New-shape v0 JSONL/SQLite replay, provenance, crash repair, compaction, snapshots, invariants, typecheck, coverage, doc-sync, build, and hygiene pass. diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index b4420e80c7..51a8f52082 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -940,7 +940,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolExecutionResult', - declaration: 'export interface ToolExecutionResult {\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: ToolErrorInfo;\n additionalContext?: HookContext;\n meta?: unknown;\n}', + declaration: 'export interface ToolExecutionResult {\n content: ContentBlock[];\n isError: boolean;\n error?: ToolErrorInfo;\n additionalContext?: HookContext;\n meta?: unknown;\n}', }, { name: 'ToolExecutionToken', diff --git a/packages/core/agent-loop/src/loop.ts b/packages/core/agent-loop/src/loop.ts index 7a7ca2e28c..38e92d8586 100644 --- a/packages/core/agent-loop/src/loop.ts +++ b/packages/core/agent-loop/src/loop.ts @@ -899,12 +899,8 @@ async function runStep( }) session.append('tool/result', { turn, step, - // The correlation id MUST be the loop's authoritative call.id (the - // model-transcript id that deriveMessages turns into toolCallId), NOT - // result.callId — a post-execute waterfall listener returning a - // mismatched id would otherwise orphan the call↔result pairing in the - // next model request. A listener-internal id, if ever needed, belongs in - // a separate diagnostic field, never overloaded onto callId. + // Correlation comes from the immutable execution input; the result does + // not duplicate this authoritative transcript identity. callId: call.id, content: result.content, isError: result.isError, diff --git a/packages/core/agent-loop/tests/review-fixes.spec.ts b/packages/core/agent-loop/tests/review-fixes.spec.ts index 5898a82ee2..9eb33672bc 100644 --- a/packages/core/agent-loop/tests/review-fixes.spec.ts +++ b/packages/core/agent-loop/tests/review-fixes.spec.ts @@ -1078,8 +1078,7 @@ describe('tool result call identity', () => { // A post-execute listener transforms the result (accept-with-replacement). // The loop must still record the tool/result under the model's authoritative - // call.id (the loop ignores result.callId — which the registry always sets to - // exec.callId anyway — and uses call.id, the model-transcript id). + // call.id, which is the immutable identity carried by the execution input. ctx.on('tools/post-execute', (exec, _result) => { expect(exec.callId).toBe(CallId('c1')) // the loop passed the real id in return Promise.resolve({ kind: 'accept', content: [{ type: 'text', text: 'ok' }] }) @@ -1089,8 +1088,7 @@ describe('tool result call identity', () => { send(agent, 'use tool') await waitForIdle(ctx, agent) - // The logged tool/result.callId is the originating call.id, NOT the - // listener's wrong id. + // The logged tool/result.callId is the originating call.id. const resultEvent = [...agent.session.events].find(e => e.type === 'tool/result') expect(resultEvent?.type).toBe('tool/result') if (resultEvent?.type === 'tool/result') { diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 97b18b0504..bda9ee860f 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -35,7 +35,7 @@ Plain class (not a Cordis Service). Create via `ctx.sessions.create()`. - `session.append(type, data, opts?): SessionEvent` — synchronous, never blocks on I/O. At this durable boundary, data and surface metadata are lossless-JSON snapshotted and deep-frozen. For an attached session, a reentrant append during dispatch/observer publication rejects, and detach waits for that publication to unwind. Callbacks resolve before the log push; the push is the commit point, after which each observer failure is contained independently. Runtime surface validation covers widened unions and raw seed/load logs. - `session.deriveMessages(): Message[]` — the LLM message history, CACHED: each surface node is projected exactly once, when first seen (O(new nodes) per call; a surface rewrite rebuilds via `surface.replaceGeneration`). Returns a fresh array per call over shared, deep-frozen `Message` objects. Each projection reuses the already deep-frozen content in its durable log event, so no second deep clone is needed and a consumer still cannot mutate logged data. The surface is the single source of derived history — there is no raw-log fallback. - `session.deriveEventMessage(event): Message | null` — the per-event projection `deriveMessages()` folds: a fresh message wrapper that reuses the event's already frozen content, or `null` when the event produces none (a non-surface event, or an empty-content `assistant/message` hosting only usage). External reconstructors and the dev invariant fold the same function over a log prefix's surface, so no two paths can disagree about what a request's messages were (the reconstructability RFC). -- `session.surface: SurfaceManager` — the derived surface, lazily rebuilt from `surfaceOp` markers in the log. Processes only new events (delta) on each access — the log is append-only, so prior events never change. `surface.replaceGeneration` is the rewrite signal: bumped by every folded `replace` and by `invalidate()`, never reset, so an incremental consumer comparing generations cannot be fooled. +- `session.surface: SurfaceManager` — the derived surface, lazily built from `surfaceOp` markers in the log. Processes only new events (delta) on each access — the log is append-only, so prior events never change. `surface.replaceGeneration` is the rewrite signal, bumped by every folded `replace`, so an incremental consumer knows when to rebuild. - `session.events` — a cached, frozen array snapshot over deep-frozen events. Repeated reads without an append return the same array; an append invalidates the cache and the next read returns a new snapshot, while earlier snapshots stay unchanged. Neither a cast nor a retained reference can push into the live log or rewrite an accepted event. - `session.seq`, `session.id` — current sequence and readonly typed identity. - `session.header: SessionHeader` — detached, deep-frozen creation metadata (`version`, `id`, `createdAt`, optional `cwd`/`parentSession`/`seedLength`). Construction validates the durable record and requires its id to match `session.id`. diff --git a/packages/core/session/src/surface.ts b/packages/core/session/src/surface.ts index 7219856bdb..20b89b27ae 100644 --- a/packages/core/session/src/surface.ts +++ b/packages/core/session/src/surface.ts @@ -73,36 +73,21 @@ export class SurfaceManager { private _nodes: SurfaceNode[] = [] /** Map from event seq → node. */ private _nodeBySeq = new Map() - /** The last processed seq. -1 forces a full rebuild on first access. */ + /** The last processed seq. -1 marks the initial lazy build. */ private _lastProcessedSeq = -1 - /** Rewrite generation — see {@link replaceGeneration}. */ + /** Replacement generation — see {@link replaceGeneration}. */ private _replaceGeneration = 0 constructor(private log: readonly SessionEvent[]) {} /** - * Reset to unprocessed state. Call after the log has been replaced - * wholesale (e.g. after Session seed). Not needed for normal appends — - * those are picked up incrementally. - */ - invalidate(): void { - this._lastProcessedSeq = -1 - this._nodes = [] - this._nodeBySeq.clear() - // A wholesale rebuild is a rewrite: bump the generation so incremental - // consumers (the session's derived-message cache) discard their view. - this._replaceGeneration += 1 - } - - /** - * The surface's rewrite generation: bumped by every folded `replace` op and - * by {@link invalidate}. A replace is the ONE operation that rewrites the - * surface non-monotonically, so an incremental consumer of {@link nodes} - * (the session's derived-message cache) compares this between visits — an - * unchanged generation guarantees every node it has not seen is a pure tail - * append; a changed one means its view must rebuild. Monotonic: it never - * moves backwards, so comparisons cannot be fooled by a re-fold. + * The surface's replacement generation, bumped by every folded `replace` op. + * A replace is the ONE operation that rewrites the surface non-monotonically, + * so an incremental consumer of {@link nodes} (the session's derived-message + * cache) compares this between visits — an unchanged generation guarantees + * every node it has not seen is a pure tail append; a changed one means its + * view must rebuild. */ get replaceGeneration(): number { if (this._lastProcessedSeq < this.log.length - 1) this._processDelta() diff --git a/packages/core/session/tests/derived-cache.spec.ts b/packages/core/session/tests/derived-cache.spec.ts index 46015106c8..66e99de625 100644 --- a/packages/core/session/tests/derived-cache.spec.ts +++ b/packages/core/session/tests/derived-cache.spec.ts @@ -1,7 +1,7 @@ /** * Derived-message cache tests: the session projects each surface node exactly - * once (O(new nodes) per call), rebuilds on a surface rewrite (replace / - * invalidate — the replaceGeneration signal), returns a fresh array snapshot + * once (O(new nodes) per call), rebuilds on a surface replacement (the + * replaceGeneration signal), returns a fresh array snapshot * per call over shared frozen messages, and stays deep-equal to a from-scratch * replay derivation at every step — the incremental==scratch property the * reconstructability RFC's invariant enforces in dev at request time. @@ -66,17 +66,6 @@ describe('derived-message cache', () => { expect(Object.isFrozen(first[0])).toBe(true) }) - it('rebuilds after surface.invalidate() (the generation covers wholesale rebuilds too)', () => { - const session = new Session(SessionId('cache-invalidate')) - session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - userText(session, 'one') - const before = session.deriveMessages() - session.surface.invalidate() - const after = session.deriveMessages() - expect(after).toEqual(before) - // A rebuild re-projects: fresh objects, same values. - expect(after[0]).not.toBe(before[0]) - }) }) describe('Session.deriveEventMessage — the per-event projection', () => { diff --git a/packages/core/session/tests/surface.spec.ts b/packages/core/session/tests/surface.spec.ts index 257828e466..0d64fd6807 100644 --- a/packages/core/session/tests/surface.spec.ts +++ b/packages/core/session/tests/surface.spec.ts @@ -28,14 +28,6 @@ describe('SurfaceManager', () => { expect(nodes[1]!.next).toBeNull() }) - it('invalidate resets to full rebuild', () => { - const s = surfaceSession() - expect(s.surface.nodes.length).toBe(2) - // After invalidate, the surface should rebuild from scratch on next access. - ;(s.surface).invalidate() - expect(s.surface.nodes.length).toBe(2) // same result, but rebuilt - }) - it('empty surface yields empty nodes', () => { const s = new Session(SessionId('empty')) // Only turn boundaries, no surface nodes. @@ -336,7 +328,7 @@ describe('surface type guards', () => { }) describe('SurfaceManager.replaceGeneration', () => { - it('folds the pending log delta on access and counts replaces and invalidations', () => { + it('folds the pending log delta on access and counts replacements', () => { const s = new Session(SessionId('gen')) s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) s.append('user/message', { content: [{ type: 'text', text: 'one' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) @@ -350,10 +342,5 @@ describe('SurfaceManager.replaceGeneration', () => { content: [{ type: 'text', text: 'summary' }], source: { kind: 'plugin', plugin: 'compact' }, }, { surfaceOp: { op: 'replace', start: nodes[0]!.seq, end: nodes[1]!.seq }, sourceEventSeqs: [nodes[0]!.seq, nodes[1]!.seq] }) expect(s.surface.replaceGeneration).toBe(1) - - // invalidate() is a rewrite too: the generation moves forward (and the - // refold re-counts the replace), never backwards. - s.surface.invalidate() - expect(s.surface.replaceGeneration).toBeGreaterThan(1) }) }) diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index d9d19c7f15..b886b4c774 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -360,7 +360,7 @@ export class SystemPrompt extends Service { private scopedVariableProviders = new Map string | undefined>>() private readonly toolOrder: string[] | undefined - constructor(ctx: Context, public config: Config) { + constructor(ctx: Context, config: Config) { super(ctx, 'systemPrompt') this.toolOrder = validateToolOrder(config.toolOrder) // The harness-owned openers. They live HERE (not on the loop plugin) so a diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index a9411f6cba..d57aeba10b 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -36,7 +36,7 @@ The live registry pipeline has three transformable waterfalls followed by the ob - `ToolExecutionInput` — the caller-supplied call description: `{ callId, name, arguments, agent?, parent?, signal? }`; callers may pass an enclosing execution's opaque token as `parent` but never choose the new execution's own token. - `ToolExecutionToken` — a fresh branded `Symbol` assigned by the registry. It supports equality correlation only and never crosses a model, log, or worker boundary. - `ToolExecution` — the pipeline-owned call: immutable `{ token, callId, name, arguments, agent?, parent? }` identity plus optional operational `signal`, which an around wrapper may add, replace, remove, and restore. A nested call's `parent` is a `ToolExecutionToken`, not an execution object. -- `ToolExecutionResult` — losslessly JSON-serializable outcome: `{ callId, content, isError, error?, additionalContext?, meta? }`. The registry materializes and freezes the complete post-policy value before final observation. On failure with a `HarnessError`, `error: { name, code }` carries the structured failure class alongside the model-facing text. +- `ToolExecutionResult` — losslessly JSON-serializable outcome: `{ content, isError, error?, additionalContext?, meta? }`. Call identity stays on the immutable `ToolExecution` supplied alongside the result instead of being duplicated on the outcome. The registry materializes and freezes the complete post-policy value before final observation. On failure with a `HarnessError`, `error: { name, code }` carries the structured failure class alongside the model-facing text. - `PreToolDecision` — `{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`. Input rewrite is deliberately not offered; `ask` is serviced by [`ctx.approval`](../../ui/user-approval/README.md) when mounted and otherwise degrades to deny. - `PostToolDecision` — `{kind:'accept', content?, additionalContext?}` (keep the call successful, optionally replacing the model-facing content) | `{kind:'block', feedback, additionalContext?}` (turn it into an `isError` whose content is the corrective feedback). Output replacement is clean because `tool/result` is logged AFTER `execute()` returns. - `ToolGuard` — `(execution) => string | undefined`; the returned string is a final monotonic denial reason evaluated after the reorderable pre-execute waterfall and before dispatch. diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 08edb8d8ae..a44d054c89 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -290,7 +290,7 @@ export interface ToolErrorInfo { * distinguish it from a tool body's own error. */ export class ToolNotFoundError extends HarnessError { - constructor(public readonly toolName: string) { + constructor(toolName: string) { super(`unknown tool "${toolName}"`, 'UNKNOWN_TOOL') this.name = 'ToolNotFoundError' } @@ -298,7 +298,6 @@ export class ToolNotFoundError extends HarnessError { /** The outcome of one tool call. */ export interface ToolExecutionResult { - callId: CallId content: ContentBlock[] isError: boolean /** @@ -918,7 +917,7 @@ export class ToolRegistry extends Service { } } catch (error: unknown) { execution = { ...base, arguments: undefined } - const result = this.materializeFinalResult(toolErrorResult(callId, error)) + const result = this.materializeFinalResult(toolErrorResult(error)) this.notifyResult(execution, result) return result } @@ -928,7 +927,7 @@ export class ToolRegistry extends Service { } catch (error: unknown) { // Outer backstop: a throwing pre/post-execute listener, guard, or the // waterfall machinery becomes an isError result, never a turn failure. - result = this.materializeFinalResult(toolErrorResult(execution.callId, error)) + result = this.materializeFinalResult(toolErrorResult(error)) } this.notifyResult(execution, result) return result @@ -953,7 +952,6 @@ export class ToolRegistry extends Service { // Every non-grant, including a failed/unavailable approval request, takes // the same deny path and still reaches post-policy plus result observers. const denied: ToolExecutionResult = { - callId: exec.callId, content: [{ type: 'text', text: `Error: ${denialReason}` }], isError: true, } @@ -984,16 +982,12 @@ export class ToolRegistry extends Service { const returned = await tool.execute(exec.arguments, exec) const content = Array.isArray(returned) ? returned : returned.content const meta = Array.isArray(returned) ? undefined : returned.meta - return { callId: exec.callId, content, isError: false, ...meta !== undefined ? { meta } : {} } + return { content, isError: false, ...meta !== undefined ? { meta } : {} } } catch (error: unknown) { - return toolErrorResult(exec.callId, error) + return toolErrorResult(error) } }, ) - if (result.callId !== exec.callId) { - throw new TypeError(`tools/execute returned callId "${String(result.callId)}" for authoritative call "${exec.callId}"`) - } - return await this.postExecute(exec, result) } @@ -1068,7 +1062,6 @@ export class ToolRegistry extends Service { const additionalContext = decision.additionalContext if (decision.kind === 'block') { return { - callId: result.callId, content: decision.feedback, isError: true, ...additionalContext ? { additionalContext } : {}, @@ -1097,10 +1090,9 @@ function createExecutionToken(): ToolExecutionToken { return Symbol('dsh.tool.execution') as ToolExecutionToken } -function toolErrorResult(callId: ToolExecution['callId'], error: unknown): ToolExecutionResult { +function toolErrorResult(error: unknown): ToolExecutionResult { const info = errorInfo(error) return { - callId, content: [{ type: 'text', text: `Error: ${errorMessage(error)}` }], isError: true, ...info ? { error: info } : {}, diff --git a/packages/core/tools/tests/scoped.spec.ts b/packages/core/tools/tests/scoped.spec.ts index 6599112c2a..d4851cdbd7 100644 --- a/packages/core/tools/tests/scoped.spec.ts +++ b/packages/core/tools/tests/scoped.spec.ts @@ -548,7 +548,6 @@ describe('scoped execution dispatch', () => { expect(reads).toBe(1) expect(result).toEqual({ - callId: CallId('unstable-arguments'), content: [{ type: 'text', text: 'ran:t' }], isError: false, }) @@ -564,10 +563,9 @@ describe('scoped execution dispatch', () => { ctx.on('internal/dispatch', (mode, name) => { if (name === 'tools/result') dispatchModes.push(mode) }) - ctx.on('tools/execute', async (exec, next) => { + ctx.on('tools/execute', async (_exec, next) => { await next() return { - callId: exec.callId, content: [{ type: 'text', text: 'outer failure' }], isError: true, } diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index e74766c699..a2dff84287 100644 --- a/packages/core/tools/tests/tools.spec.ts +++ b/packages/core/tools/tests/tools.spec.ts @@ -80,7 +80,7 @@ describe('ToolRegistry', () => { const ctx = await setup() ctx.tools.register(echoTool) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'hi' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'hi' }], isError: false }) }) it('threads a tool-attached meta (object return form) onto the result', async () => { @@ -94,7 +94,6 @@ describe('ToolRegistry', () => { }) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'meta-tool', arguments: {} }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false, meta: { diffs: [{ path: 'a', oldText: null, newText: 'x' }] }, @@ -111,7 +110,7 @@ describe('ToolRegistry', () => { }, }) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'no-meta-tool', arguments: {} }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'ok' }], isError: false }) expect('meta' in result).toBe(false) }) @@ -178,13 +177,12 @@ describe('ToolRegistry', () => { }) }) - it('ToolNotFoundError carries the tool name and a stable code', async () => { + it('ToolNotFoundError carries a stable message and code', async () => { const { HarnessError } = await import('@deepseek-ai/dsh-llm') const err = new ToolNotFoundError('ghost') expect(err).toBeInstanceOf(HarnessError) expect(err.name).toBe('ToolNotFoundError') expect(err.code).toBe('UNKNOWN_TOOL') - expect(err.toolName).toBe('ghost') expect(err.message).toBe('unknown tool "ghost"') }) @@ -425,7 +423,7 @@ describe('ToolRegistry', () => { ctx.on('tools/post-execute', async (_exec, _result, next) => { order.push('post'); return next() }) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'traced', arguments: { text: 'hi' } }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'hi' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'hi' }], isError: false }) // The around seam wraps dispatch; pre gates before it, post runs over its result. expect(order).toEqual(['pre', 'execute:before', 'dispatch', 'execute:after', 'post']) }) @@ -526,8 +524,8 @@ describe('ToolRegistry', () => { async execute() { dispatched = true; return [] }, }) - ctx.on('tools/execute', async (exec: ToolExecution, _next: () => Promise): Promise => - ({ callId: exec.callId, content: [{ type: 'text', text: 'short-circuited' }], isError: false })) + ctx.on('tools/execute', async (_exec: ToolExecution, _next: () => Promise): Promise => + ({ content: [{ type: 'text', text: 'short-circuited' }], isError: false })) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'never-runs', arguments: {} }) expect(dispatched).toBe(false) // returning without next() skips core dispatch @@ -537,8 +535,7 @@ describe('ToolRegistry', () => { it('preserves additionalContext supplied by an around-dispatch result', async () => { const ctx = await setup() ctx.tools.register(echoTool) - ctx.on('tools/execute', async exec => ({ - callId: exec.callId, + ctx.on('tools/execute', async () => ({ content: [{ type: 'text', text: 'short-circuited with context' }], isError: false, additionalContext: { @@ -556,20 +553,6 @@ describe('ToolRegistry', () => { }) }) - it('normalizes a tools/execute result with the wrong call id', async () => { - const ctx = await setup() - ctx.tools.register(echoTool) - ctx.on('tools/execute', async () => ({ callId: CallId('other'), content: [], isError: false })) - - const result = await ctx.tools.execute({ - callId: CallId('malformed-shape'), name: 'echo', arguments: {}, - }) - expect(result.isError).toBe(true) - expect(result.content[0]).toMatchObject({ - text: 'Error: tools/execute returned callId "other" for authoritative call "malformed-shape"', - }) - }) - it('returns an isError result when a tools/execute listener throws', async () => { const ctx = await setup() ctx.tools.register(echoTool) @@ -577,7 +560,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: wrapper broke' }], isError: true, }) @@ -593,7 +575,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: permission hook broke' }], isError: true, }) @@ -609,7 +590,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: post hook broke' }], isError: true, }) @@ -625,7 +605,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toMatchObject({ - callId: CallId('c1'), isError: true, error: { name: 'HarnessError', code: 'DENIED' }, }) @@ -1263,7 +1242,7 @@ describe('defineTool validation (the runtime-validation RFC, part 1)', () => { }, })) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'reader', arguments: { path: '/x' } }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'read /x' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'read /x' }], isError: false }) }) it('ToolArgsError carries a stable code and the violation list', () => { diff --git a/packages/support/invariants/tests/invariants.spec.ts b/packages/support/invariants/tests/invariants.spec.ts index a1e824524b..d82affe4e7 100644 --- a/packages/support/invariants/tests/invariants.spec.ts +++ b/packages/support/invariants/tests/invariants.spec.ts @@ -869,9 +869,9 @@ describe('scoped-dispatch invariants', () => { ['agent/error', [agent, 1, 0, new Error('x')]], ['approval/request', [{ agent, toolName: 'echo' }, () => Promise.resolve('unavailable')]], ['tools/pre-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ kind: 'allow' })]], - ['tools/execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ callId: 'c', content: [], isError: false })]], - ['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { callId: 'c', content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]], - ['tools/result', [{ callId: 'c', name: 't', arguments: {}, agent }, { callId: 'c', content: [], isError: false }]], + ['tools/execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ content: [], isError: false })]], + ['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]], + ['tools/result', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }]], ] for (const [event, args] of rows) { const subject = agent diff --git a/packages/timeout/timeout-policy/src/index.ts b/packages/timeout/timeout-policy/src/index.ts index 2e9ed0d635..49f62f9e81 100644 --- a/packages/timeout/timeout-policy/src/index.ts +++ b/packages/timeout/timeout-policy/src/index.ts @@ -33,7 +33,6 @@ */ import type { Context } from 'cordis' -import type { CallId } from '@deepseek-ai/dsh-llm' import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools' @@ -57,13 +56,11 @@ export const inject = ['tools'] * is the model-facing message; `error.code` is the same {@link TOOL_TIMEOUT} * this plugin owns, so a retry/sandbox plugin (and replay) can route on it. * - * @param callId - the timed-out call's id, carried onto the replacement result. * @param timeoutMs - the elapsed budget, rendered into the model-facing message. * @returns the `isError` {@link ToolExecutionResult} with a `TOOL_TIMEOUT` error. */ -export function toolTimeoutResult(callId: CallId, timeoutMs: number): ToolExecutionResult { +function toolTimeoutResult(timeoutMs: number): ToolExecutionResult { return { - callId, content: [{ type: 'text', text: `Error: tool call timed out after ${timeoutMs}ms` }], isError: true, error: { name: 'ToolTimeoutError', code: TOOL_TIMEOUT }, @@ -108,7 +105,7 @@ export function apply(ctx: Context): void { // quiescence; replace whatever it returned (its own abort result) with the // structured TOOL_TIMEOUT the model sees. if (timeoutOf(d.signal, TOOL_TIMEOUT) !== undefined) { - return toolTimeoutResult(exec.callId, timeoutMs) + return toolTimeoutResult(timeoutMs) } return result } finally { diff --git a/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts b/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts index 30a7307515..bd06ed6e16 100644 --- a/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts +++ b/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts @@ -11,9 +11,9 @@ import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import { CallId, HarnessError } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' -import ToolRegistry, { defineTool, type ToolExecutionInput, type ToolExecutionResult, type PostToolDecision } from '@deepseek-ai/dsh-tools' +import ToolRegistry, { defineTool, type ToolExecutionInput, type PostToolDecision } from '@deepseek-ai/dsh-tools' import * as timeoutPolicy from '@deepseek-ai/dsh-timeout-policy' -import { TOOL_TIMEOUT, toolTimeoutResult } from '@deepseek-ai/dsh-timeout-policy' +import { TOOL_TIMEOUT } from '@deepseek-ai/dsh-timeout-policy' /** Mount the registry + the zero-config timeout-policy enforcer. */ async function setup() { @@ -60,7 +60,7 @@ describe('timeout-policy delegation (unconfigured / fast)', () => { ctx.tools.register(defineTool({ name: 'fast', description: 'd', parameters: {}, timeoutMs: 10_000, async execute() { return [{ type: 'text' as const, text: 'ok' }] } })) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'fast', arguments: {} }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'ok' }], isError: false }) }) it('a budgeted tool receives the DERIVED deadline signal (not the caller signal) during dispatch', async () => { @@ -109,7 +109,6 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { await vi.advanceTimersByTimeAsync(150) const result = await pending expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: tool call timed out after 100ms' }], isError: true, error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, @@ -140,16 +139,7 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { }) }) -describe('toolTimeoutResult', () => { - it('builds the structured TOOL_TIMEOUT result', () => { - expect(toolTimeoutResult(CallId('c9'), 250)).toEqual({ - callId: CallId('c9'), - content: [{ type: 'text', text: 'Error: tool call timed out after 250ms' }], - isError: true, - error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, - } satisfies ToolExecutionResult) - }) - +describe('timeout-policy contract', () => { it('exposes the owned code constant', () => { expect(TOOL_TIMEOUT).toBe('TOOL_TIMEOUT') }) From f9db1a6a08b4af895f33b8d01957801aa727714e Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:30:23 +0800 Subject: [PATCH 02/21] refactor: hide filesystem implementation helpers --- docs/config-catalog.md | 4 ++-- packages/fs/fs-local/README.md | 2 +- packages/fs/fs-local/src/index.ts | 14 -------------- packages/fs/fs-local/tests/fsio.spec.ts | 4 ++-- packages/fs/tool-fs/README.md | 2 +- packages/fs/tool-fs/src/index.ts | 9 --------- packages/fs/tool-fs/tests/diff.spec.ts | 2 +- packages/fs/tool-fs/tests/read-render.spec.ts | 4 ++-- packages/fs/tool-fs/tests/tools.spec.ts | 5 +++-- 9 files changed, 12 insertions(+), 34 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9d4b8038b5..a5eb611f02 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -265,7 +265,7 @@ export interface Config { } ``` -Source: [`packages/fs/fs-local/src/index.ts:58`](../packages/fs/fs-local/src/index.ts) +Source: [`packages/fs/fs-local/src/index.ts:44`](../packages/fs/fs-local/src/index.ts) ## `@deepseek-ai/dsh-hooks-claude` @@ -851,7 +851,7 @@ export interface Config { } ``` -Source: [`packages/fs/tool-fs/src/index.ts:48`](../packages/fs/tool-fs/src/index.ts) +Source: [`packages/fs/tool-fs/src/index.ts:39`](../packages/fs/tool-fs/src/index.ts) ## `@deepseek-ai/dsh-tool-skill` diff --git a/packages/fs/fs-local/README.md b/packages/fs/fs-local/README.md index 26524fad24..a2ea5de742 100644 --- a/packages/fs/fs-local/README.md +++ b/packages/fs/fs-local/README.md @@ -23,4 +23,4 @@ await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) `config.cwd` is a resolution default, not a containment boundary — absolute paths and `..` escape it. Enforce containment with a stricter `ctx.fs` backend or a permission plugin on the `tools/execute` waterfall. See [the filesystem capability-seam RFC's Consequences section](../../../docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md#consequences). -The raw I/O lives in `src/fsio.ts` (Cordis-free, independently unit-tested); `src/index.ts` is the thin service wiring. +The package-root SDK surface is the default/named `LocalFileSystem` class plus `Config`. Raw I/O lives in `src/fsio.ts` (Cordis-free, independently unit-tested); `src/index.ts` is the thin service wiring. diff --git a/packages/fs/fs-local/src/index.ts b/packages/fs/fs-local/src/index.ts index 1a3ebc57f6..8a9294a4a6 100644 --- a/packages/fs/fs-local/src/index.ts +++ b/packages/fs/fs-local/src/index.ts @@ -40,20 +40,6 @@ import { } from './fsio.ts' import type { FsIoInternals } from './fsio.ts' -export { - applyLiteralEdit, - listDirectory, - probe, - readForEdit, - readTextForDiff, - readWholeText, - resolveLocalTarget, - restoreLineEndings, - streamWholeText, - writeFileAtomic, -} from './fsio.ts' -export type { FsIoInternals, LineEndings, LocalDirEntry, LocalTarget, PathInfo } from './fsio.ts' - /** Configuration for the local filesystem backend. */ export interface Config { /** Base directory for relative paths. Defaults to `process.cwd()`. */ diff --git a/packages/fs/fs-local/tests/fsio.spec.ts b/packages/fs/fs-local/tests/fsio.spec.ts index 3a30f73ed2..6723ae9d9c 100644 --- a/packages/fs/fs-local/tests/fsio.spec.ts +++ b/packages/fs/fs-local/tests/fsio.spec.ts @@ -20,8 +20,8 @@ import { restoreLineEndings, streamWholeText, writeFileAtomic, -} from '@deepseek-ai/dsh-fs-local' -import type { LocalTarget } from '@deepseek-ai/dsh-fs-local' +} from '../src/fsio.ts' +import type { LocalTarget } from '../src/fsio.ts' import { FsError, FsTargetKey } from '@deepseek-ai/dsh-fs' let dir: string diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index fedd1ff7a2..a72bfaf201 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -46,4 +46,4 @@ The tool passes `exec` (the tool-execution context) as the opaque `actor` on eve `fs/observed` fires AFTER the read/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`); the tool does not guard the emit, so a listener that throws would surface as the tool's `isError` result — async or fallible observation does not belong on this event. -The read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them. +The package root exports only the Cordis plugin contract (`name`, `inject`, `Config`, and `apply`). Read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them. diff --git a/packages/fs/tool-fs/src/index.ts b/packages/fs/tool-fs/src/index.ts index f5d0d9ef91..83e19bb18a 100644 --- a/packages/fs/tool-fs/src/index.ts +++ b/packages/fs/tool-fs/src/index.ts @@ -29,15 +29,6 @@ import { applyWriteTool } from './write.ts' import { applyEditTool } from './edit.ts' import { READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from './read-render.ts' -export { READ_LIMIT, STREAM_MIN_SIZE, applyReadTool, parseReadArgs } from './read.ts' -export type { ReadToolCaps } from './read.ts' -export { applyWriteTool, formatWriteOutput, parseWriteArgs } from './write.ts' -export { applyEditTool, formatEditOutput, parseEditArgs } from './edit.ts' -export { READ_MAX_BYTES, READ_MAX_LINE_LENGTH, buildWindow, formatReadOutput } from './read-render.ts' -export type { FileReadOutcome, FileTextLine, ReadWindow, WindowResult } from './read-render.ts' -export { DIFF_CONTEXT, computeHunkDiffs, diffsFromMeta } from './diff.ts' -export type { FsDiffMeta } from './diff.ts' - /** Cordis plugin name used by loader diagnostics. */ export const name = 'tool-fs' diff --git a/packages/fs/tool-fs/tests/diff.spec.ts b/packages/fs/tool-fs/tests/diff.spec.ts index 12ab7209b6..21f977f0fa 100644 --- a/packages/fs/tool-fs/tests/diff.spec.ts +++ b/packages/fs/tool-fs/tests/diff.spec.ts @@ -6,7 +6,7 @@ */ import { describe, expect, it } from 'vitest' -import { computeHunkDiffs, diffsFromMeta, DIFF_CONTEXT } from '@deepseek-ai/dsh-tool-fs' +import { computeHunkDiffs, diffsFromMeta, DIFF_CONTEXT } from '../src/diff.ts' import type { JsonValue } from '@deepseek-ai/dsh-session' const lines = (n: number): string => Array.from({ length: n }, (_, i) => `line${i + 1}`).join('\n') + '\n' diff --git a/packages/fs/tool-fs/tests/read-render.spec.ts b/packages/fs/tool-fs/tests/read-render.spec.ts index c23ad79170..ab4d2a618b 100644 --- a/packages/fs/tool-fs/tests/read-render.spec.ts +++ b/packages/fs/tool-fs/tests/read-render.spec.ts @@ -6,8 +6,8 @@ */ import { describe, expect, it } from 'vitest' -import { buildWindow, READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from '@deepseek-ai/dsh-tool-fs' -import type { ReadWindow } from '@deepseek-ai/dsh-tool-fs' +import { buildWindow, READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from '../src/read-render.ts' +import type { ReadWindow } from '../src/read-render.ts' const DEFAULT_CAPS = { maxLineLength: READ_MAX_LINE_LENGTH, maxBytes: READ_MAX_BYTES } const READ_ALL: ReadWindow = { offset: 1, limit: 2000, ...DEFAULT_CAPS } diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index c17bd875ba..c7e1a64cbe 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -25,8 +25,9 @@ import type { } from '@deepseek-ai/dsh-fs' import * as FsPolicy from '@deepseek-ai/dsh-fs-policy' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' -import { formatReadOutput, STREAM_MIN_SIZE } from '@deepseek-ai/dsh-tool-fs' -import type { FileReadOutcome } from '@deepseek-ai/dsh-tool-fs' +import { STREAM_MIN_SIZE } from '../src/read.ts' +import { formatReadOutput } from '../src/read-render.ts' +import type { FileReadOutcome } from '../src/read-render.ts' /** An in-memory fake provider; a test can arm a rejection on any primitive. */ class FakeFs extends FileSystem { From 8c8b422f1787233bf8485982bb92f206734a9e3f Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:37:51 +0800 Subject: [PATCH 03/21] refactor: prune bash implementation surface --- docs/config-catalog.md | 2 +- docs/core-data-structures/bash.md | 1 - packages/bash/bash-local/README.md | 2 + packages/bash/bash-local/src/index.ts | 4 -- packages/bash/bash-local/src/run.ts | 27 ++----- packages/bash/bash-local/tests/run.spec.ts | 16 ++--- packages/bash/bash/src/types.ts | 1 - packages/bash/bash/tests/service.spec.ts | 1 - packages/bash/tool-bash/README.md | 2 + packages/bash/tool-bash/src/index.ts | 65 +---------------- packages/bash/tool-bash/src/render.ts | 70 +++++++++++++++++++ packages/bash/tool-bash/tests/tools.spec.ts | 4 +- .../cordis/tool-cordis/src/api-catalog.ts | 2 +- 13 files changed, 88 insertions(+), 109 deletions(-) create mode 100644 packages/bash/tool-bash/src/render.ts diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9d4b8038b5..b0391d7a65 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -149,7 +149,7 @@ export interface Config { } ``` -Source: [`packages/bash/bash-local/src/index.ts:29`](../packages/bash/bash-local/src/index.ts) +Source: [`packages/bash/bash-local/src/index.ts:26`](../packages/bash/bash-local/src/index.ts) ## `@deepseek-ai/dsh-bash-sandbox` diff --git a/docs/core-data-structures/bash.md b/docs/core-data-structures/bash.md index 51f7cb0696..58810ecdda 100644 --- a/docs/core-data-structures/bash.md +++ b/docs/core-data-structures/bash.md @@ -202,7 +202,6 @@ A long-running command started with `start()` is tracked as a `BashTask`. `BashT ```ts type-equiv interface BashTask { readonly id: BashTaskId - readonly command: string status: BashTaskStatus /** Exit code once finished (null = killed by signal / still running). */ exitCode: number | null diff --git a/packages/bash/bash-local/README.md b/packages/bash/bash-local/README.md index 1ac8a7d06b..fde35d8aa9 100644 --- a/packages/bash/bash-local/README.md +++ b/packages/bash/bash-local/README.md @@ -2,6 +2,8 @@ Local-subprocess implementation of the `@deepseek-ai/dsh-bash` executor seam: `LocalBashExecutor` spawns `bash -c ` per call in its own process group, collects bounded output with full-stream spill files, and escalates kills SIGTERM→SIGKILL across the whole group. +The package root exports the default and named `LocalBashExecutor` plugin plus its `Config`; subprocess plumbing stays internal to the implementation package. + ## Config ```yaml diff --git a/packages/bash/bash-local/src/index.ts b/packages/bash/bash-local/src/index.ts index 6903c06e32..a90bcaf29a 100644 --- a/packages/bash/bash-local/src/index.ts +++ b/packages/bash/bash-local/src/index.ts @@ -22,9 +22,6 @@ import { clampTimeout, deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' import { DEFAULT_GRACE_MS, runBash } from './run.ts' import type { RunInternals, RunningBash } from './run.ts' -export { DEFAULT_GRACE_MS, ENV_OVERRIDES, killGroup, OutputCollector, runBash } from './run.ts' -export type { RunInternals, RunningBash, SpawnOutcome, SpawnSpec } from './run.ts' - /** Plugin config (all optional — `static Config` supplies the defaults). */ export interface Config { /** Default working directory for commands (default: process.cwd()). */ @@ -184,7 +181,6 @@ export class LocalBashExecutor extends BashExecutor { const id = BashTaskId(`bash-${this.nextTaskId++}`) const task: TrackedTask = { id, - command: spec.command, status: 'running', exitCode: null, signal: null, diff --git a/packages/bash/bash-local/src/run.ts b/packages/bash/bash-local/src/run.ts index bc4a017dea..c09aa80173 100644 --- a/packages/bash/bash-local/src/run.ts +++ b/packages/bash/bash-local/src/run.ts @@ -209,27 +209,6 @@ export class OutputCollector { writeSync(this.spillFd, chunk) } - // TODO(snapshot-scope): `snapshot()` has one internal caller (`finalize()` at - // the bottom of this file) and `totalBytes` is read only by a test. The live - // background-poll path goes through `readFrom()`, so inline snapshot() into - // finalize() and drop or privatize the totalBytes getter. - /** - * Read the collected tail without finalizing (the final-result snapshot). - * @returns the retained tail text, the truncation flag, and the spill path when one was created. - */ - snapshot(): CollectedOutput { - return { - text: Buffer.concat(this.chunks).toString('utf8'), - truncated: this.dropped, - ...this.spillFile !== undefined ? { spillPath: this.spillFile } : {}, - } - } - - /** Total bytes ever pushed (including bytes dropped from memory). */ - get totalBytes(): number { - return this.total - } - /** * Incremental read in whole-stream byte coordinates: returns everything * pushed since `fromByte`. When `fromByte` has already slid out of the @@ -270,7 +249,11 @@ export class OutputCollector { } this.spillFd = undefined } - return this.snapshot() + return { + text: Buffer.concat(this.chunks).toString('utf8'), + truncated: this.dropped, + ...this.spillFile !== undefined ? { spillPath: this.spillFile } : {}, + } } } diff --git a/packages/bash/bash-local/tests/run.spec.ts b/packages/bash/bash-local/tests/run.spec.ts index 1d6e93afe3..eef7f0b1e8 100644 --- a/packages/bash/bash-local/tests/run.spec.ts +++ b/packages/bash/bash-local/tests/run.spec.ts @@ -2,8 +2,8 @@ import { mkdtempSync, readFileSync, statSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { describe, expect, it, vi } from 'vitest' -import { killGroup, OutputCollector, runBash } from '@deepseek-ai/dsh-bash-local' -import type { RunningBash } from '@deepseek-ai/dsh-bash-local' +import { killGroup, OutputCollector, runBash } from '../src/run.ts' +import type { RunningBash } from '../src/run.ts' const { failNextClose } = vi.hoisted(() => ({ failNextClose: { value: false } })) vi.mock('node:fs', async (importOriginal) => { @@ -49,7 +49,7 @@ async function waitGone(pid: number, timeoutMs = 5_000): Promise { async function waitForStdout(running: RunningBash, expected: string, timeoutMs = 5_000): Promise { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { - if (running.stdout.snapshot().text.includes(expected)) return + if (running.stdout.readFrom(0).text.includes(expected)) return await new Promise(resolve => setTimeout(resolve, 20)) } throw new Error(`stdout did not include ${JSON.stringify(expected)} after ${timeoutMs}ms`) @@ -300,19 +300,11 @@ describe('OutputCollector', () => { expect(third.spillPath).toBeDefined() }) - it('tracks totalBytes across drops', () => { - const collector = new OutputCollector(4, 'test', spillDir) - collector.push(Buffer.from('aaaa')) - collector.push(Buffer.from('bbbb')) - expect(collector.totalBytes).toBe(8) - expect(collector.finalize().text).toBe('bbbb') - }) - it('contains close failures and drops the spill path', () => { const collector = new OutputCollector(4, 'closefail', spillDir) collector.push(Buffer.from('aaaa')) collector.push(Buffer.from('bbbb')) - expect(collector.snapshot().spillPath).toBeDefined() + expect(collector.readFrom(0).spillPath).toBeDefined() failNextClose.value = true let out: ReturnType diff --git a/packages/bash/bash/src/types.ts b/packages/bash/bash/src/types.ts index 39dbc162c6..5225d6d6b3 100644 --- a/packages/bash/bash/src/types.ts +++ b/packages/bash/bash/src/types.ts @@ -229,7 +229,6 @@ export type BashTaskStatus = 'running' | 'completed' | 'killed' /** A tracked background task handle. */ export interface BashTask { readonly id: BashTaskId - readonly command: string status: BashTaskStatus /** Exit code once finished (null = killed by signal / still running). */ exitCode: number | null diff --git a/packages/bash/bash/tests/service.spec.ts b/packages/bash/bash/tests/service.spec.ts index 94d299f175..bdee15aedd 100644 --- a/packages/bash/bash/tests/service.spec.ts +++ b/packages/bash/bash/tests/service.spec.ts @@ -34,7 +34,6 @@ class StubExecutor extends BashExecutor { start(spec: BashExecSpec): BashTask { const task: BashTask = { id: BashTaskId(`stub-${this.tasks.size + 1}`), - command: spec.command, status: 'running', exitCode: null, signal: null, diff --git a/packages/bash/tool-bash/README.md b/packages/bash/tool-bash/README.md index 800de0132c..ac55e342cd 100644 --- a/packages/bash/tool-bash/README.md +++ b/packages/bash/tool-bash/README.md @@ -4,6 +4,8 @@ The model-facing bash tools — `bash`, `bash_output`, `bash_kill` — registere Requires a loaded executor implementation (e.g. `@deepseek-ai/dsh-bash-local`); the plugin stays pending until `ctx.bash` exists (`inject: ['tools', 'bash', 'systemPrompt']`). +The package root exposes only the Cordis plugin contract (`name`, `inject`, `apply`); result rendering remains an implementation detail covered by same-package tests. + The plugin also contributes the `tool:bash` prompt section (order 105) — the cross-call habit the per-tool descriptions cannot carry: check the `[exit code: N]` marker on every result and investigate failures before moving on. Under a sandboxing executor it additionally contributes the per-agent `env:bash-sandbox` section (order 110) stating each session's EFFECTIVE mode, and the pre-step narrator — see [Per-session mode](#per-session-mode-switching-and-visibility). ## Tools diff --git a/packages/bash/tool-bash/src/index.ts b/packages/bash/tool-bash/src/index.ts index 31e6512ae0..2df80b879d 100644 --- a/packages/bash/tool-bash/src/index.ts +++ b/packages/bash/tool-bash/src/index.ts @@ -68,7 +68,8 @@ import type {} from '@deepseek-ai/dsh-system-prompt' import type {} from '@deepseek-ai/dsh-user-approval' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' import { BashTaskId, OwnerToken, effectiveSandboxMode } from '@deepseek-ai/dsh-bash' -import type { BashRunResult, BashTask, CollectedOutput } from '@deepseek-ai/dsh-bash' +import type { BashTask } from '@deepseek-ai/dsh-bash' +import { renderResult } from './render.ts' export const name = 'tool-bash' export const inject = ['tools', 'bash', 'systemPrompt'] @@ -185,68 +186,6 @@ function bashDescription(escalationModes: readonly SandboxMode[]): string { + 'it — but it does not forbid attempting or escalating other commands later.' } -/** Append the truncation notice (with the full-output spill path) to a stream's text. */ -function streamText(output: CollectedOutput): string { - if (!output.truncated) return output.text - return `${output.text}\n[output truncated; full output: ${output.spillPath ?? '(unavailable)'}]` -} - -/** - * Shape one finished run into the text the model sees: stdout, then a marked - * stderr section, then exit-status markers. Non-zero exits are REPORTED, not - * errored — the model decides how to react; only infrastructure failures - * (spawn errors, aborts) surface as isError results. - * @param result - the completed foreground run from the executor. - * @param escalationModes - the escalation targets this composition advertises; - * non-empty adds the same-turn escalation hint after a denial marker - * (default `[]`: no hint). - * @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line. - */ -export function renderResult( - result: BashRunResult, - escalationModes: readonly SandboxMode[] = [], -): string { - const out = streamText(result.stdout) - const err = streamText(result.stderr) - - let body = out - if (err.length > 0) { - // Single newline between sections (stdout usually ends with one already). - if (body.length > 0 && !body.endsWith('\n')) body += '\n' - body += `[stderr]\n${err}` - } - if (body.length === 0) body = '(no output)' - - const markers: string[] = [] - // The sandbox marker precedes the exit-status markers so `[exit code: N]` - // stays the LAST line (exitStatus() anchors its parse there). Denial is a - // reported fact like timeout: the model decides how to react. - if (result.sandbox?.denied) { - markers.push(`[sandbox: file access denied under ${result.sandbox.mode} mode]`) - // The same-turn nudge lives at the decision point: only when this - // composition advertises the fields (a lever is never hinted that the - // schema does not offer), and inside the sandbox marker family so the - // exit-code marker stays the last line. - if (escalationModes.length > 0) { - markers.push('[sandbox: escalation available — retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]') - } - } - // Timeout is reported independently of how the process actually ended: a - // command can trap SIGTERM and exit 0 after our timer fired (e.g. - // `trap "exit 0" TERM; sleep 60`), giving timedOut:true / exitCode:0 / - // signal:null — the model must still see that the command was cut short. - if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`) - if (result.signal !== null) { - markers.push(`[killed by signal: ${result.signal}]`) - } else if (result.exitCode !== 0) { - markers.push(`[exit code: ${result.exitCode}]`) - } - if (markers.length === 0) return body - - if (!body.endsWith('\n')) body += '\n' - return body + markers.join('\n') -} - // --------------------------------------------------------------------------- // UI presentation (tool-owned). These shape how a UI (e.g. the ACP bridge) // renders a bash call's pending and completed states. They are display-only and diff --git a/packages/bash/tool-bash/src/render.ts b/packages/bash/tool-bash/src/render.ts new file mode 100644 index 0000000000..f8d9398fa1 --- /dev/null +++ b/packages/bash/tool-bash/src/render.ts @@ -0,0 +1,70 @@ +/** + * Model-facing result rendering for the bash tool. + * + * @module @deepseek-ai/dsh-tool-bash/render + */ + +import type { BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' + +/** Append the truncation notice (with the full-output spill path) to a stream's text. */ +function streamText(output: CollectedOutput): string { + if (!output.truncated) return output.text + return `${output.text}\n[output truncated; full output: ${output.spillPath ?? '(unavailable)'}]` +} + +/** + * Shape one finished run into the text the model sees: stdout, then a marked + * stderr section, then exit-status markers. Non-zero exits are REPORTED, not + * errored — the model decides how to react; only infrastructure failures + * (spawn errors, aborts) surface as isError results. + * @param result - the completed foreground run from the executor. + * @param escalationModes - the escalation targets this composition advertises; + * non-empty adds the same-turn escalation hint after a denial marker + * (default `[]`: no hint). + * @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line. + */ +export function renderResult( + result: BashRunResult, + escalationModes: readonly SandboxMode[] = [], +): string { + const out = streamText(result.stdout) + const err = streamText(result.stderr) + + let body = out + if (err.length > 0) { + // Single newline between sections (stdout usually ends with one already). + if (body.length > 0 && !body.endsWith('\n')) body += '\n' + body += `[stderr]\n${err}` + } + if (body.length === 0) body = '(no output)' + + const markers: string[] = [] + // The sandbox marker precedes the exit-status markers so `[exit code: N]` + // stays the LAST line (exitStatus() anchors its parse there). Denial is a + // reported fact like timeout: the model decides how to react. + if (result.sandbox?.denied) { + markers.push(`[sandbox: file access denied under ${result.sandbox.mode} mode]`) + // The same-turn nudge lives at the decision point: only when this + // composition advertises the fields (a lever is never hinted that the + // schema does not offer), and inside the sandbox marker family so the + // exit-code marker stays the last line. + if (escalationModes.length > 0) { + markers.push('[sandbox: escalation available — retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]') + } + } + // Timeout is reported independently of how the process actually ended: a + // command can trap SIGTERM and exit 0 after our timer fired (e.g. + // `trap "exit 0" TERM; sleep 60`), giving timedOut:true / exitCode:0 / + // signal:null — the model must still see that the command was cut short. + if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`) + if (result.signal !== null) { + markers.push(`[killed by signal: ${result.signal}]`) + } else if (result.exitCode !== 0) { + markers.push(`[exit code: ${result.exitCode}]`) + } + if (markers.length === 0) return body + + if (!body.endsWith('\n')) body += '\n' + return body + markers.join('\n') +} diff --git a/packages/bash/tool-bash/tests/tools.spec.ts b/packages/bash/tool-bash/tests/tools.spec.ts index 12b4a53958..1e078a6457 100644 --- a/packages/bash/tool-bash/tests/tools.spec.ts +++ b/packages/bash/tool-bash/tests/tools.spec.ts @@ -19,7 +19,7 @@ import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' import ApprovalService from '@deepseek-ai/dsh-user-approval' import type { ApprovalOutcome } from '@deepseek-ai/dsh-user-approval' import * as ToolBash from '@deepseek-ai/dsh-tool-bash' -import { renderResult } from '@deepseek-ai/dsh-tool-bash' +import { renderResult } from '../src/render.ts' const spillDir = mkdtempSync(join(tmpdir(), 'dsh-tool-bash-spec-')) @@ -118,7 +118,6 @@ abstract class TestBashExecutor extends BashExecutor { class LossyReadBashExecutor extends TestBashExecutor { private readonly task: BashTask = { id: BashTaskId('bash-lossy'), - command: 'fake', status: 'running', exitCode: null, signal: null, @@ -1062,7 +1061,6 @@ describe('sandbox rendering', () => { class FactsOnlyExecutor extends TestBashExecutor { private readonly task: BashTask = { id: BashTaskId('bash-facts'), - command: 'fake', status: 'completed', exitCode: 1, signal: null, diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index b4420e80c7..b43a0492b7 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -564,7 +564,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'BashTask', - declaration: 'export interface BashTask {\n readonly id: BashTaskId;\n readonly command: string;\n status: BashTaskStatus;\n exitCode: number | null;\n signal: NodeJS.Signals | null;\n readonly done: Promise;\n sandbox?: BashSandboxInfo;\n}', + declaration: 'export interface BashTask {\n readonly id: BashTaskId;\n status: BashTaskStatus;\n exitCode: number | null;\n signal: NodeJS.Signals | null;\n readonly done: Promise;\n sandbox?: BashSandboxInfo;\n}', }, { name: 'BashTaskId', From 419370ea4b7afdac2d3ae1d6d5f7248290823f71 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:45:22 +0800 Subject: [PATCH 04/21] fix: keep bash status rendering and parsing together --- packages/bash/tool-bash/src/index.ts | 35 +-------------------------- packages/bash/tool-bash/src/render.ts | 22 +++++++++++++++++ 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/packages/bash/tool-bash/src/index.ts b/packages/bash/tool-bash/src/index.ts index 2df80b879d..af013de21e 100644 --- a/packages/bash/tool-bash/src/index.ts +++ b/packages/bash/tool-bash/src/index.ts @@ -69,7 +69,7 @@ import type {} from '@deepseek-ai/dsh-user-approval' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' import { BashTaskId, OwnerToken, effectiveSandboxMode } from '@deepseek-ai/dsh-bash' import type { BashTask } from '@deepseek-ai/dsh-bash' -import { renderResult } from './render.ts' +import { parseExitStatus, renderResult } from './render.ts' export const name = 'tool-bash' export const inject = ['tools', 'bash', 'systemPrompt'] @@ -275,39 +275,6 @@ function presentBashResult(args: unknown, result: ToolResult): ToolResultView | return { card: 'terminal', output: raw, ...parseExitStatus(raw) } } -/** - * Recover the structured exit status from a rendered `renderResult` string — the - * inverse of the status markers it appends. A `[killed by signal: SIG]` marker - * yields `{signal}`; otherwise an `[exit code: N]` marker yields `{exitCode:N}`; - * absent both we report `{exitCode:0}` (a clean run appends no marker — and a - * trapped-timeout run that exits 0 also has none and is accurately exit 0). - * - * Why parse rendered text at all: `presentResult` is replay-safe and on a - * `session/load` the ONLY thing persisted is this content text — the structured - * `BashRunResult` is long gone — so unless the exit were added to the persisted - * event schema (deliberately NOT done; see the terminal-rendering RFC), parsing - * is the only channel. The match is anchored to a LEADING newline + end-of-string - * because `renderResult` always inserts a `\n` before the marker (line ~124) onto - * a non-empty body: a real marker is therefore always its own final line. That - * defeats the common spoof (program output that simply ENDS in `[exit code: 5]` - * with no trailing newline — a clean exit 0 — no longer reads as a failure). - * - * KNOWN RESIDUAL (inherent to the replay-only-sees-text design): a clean exit 0 - * whose body's FINAL line is itself exactly the marker text — `[exit code: N]` - * or `[killed by signal: SIG]`, printed by the program with nothing after — is - * still indistinguishable from a real marker and would show a wrong pill. This is - * display-only (execution and the model-facing text are unaffected) and narrow; - * the complete fix is to persist a structured exit on the result event, which the - * RFC names as the escape hatch. - */ -function parseExitStatus(text: string): { exitCode: number } | { signal: string } { - const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text) - if (signal?.[1] !== undefined) return { signal: signal[1] } - const exit = /\n\[exit code: (\d+)\]$/.exec(text) - if (exit?.[1] !== undefined) return { exitCode: Number(exit[1]) } - return { exitCode: 0 } -} - /** Pending-state presentation for `bash_output`/`bash_kill` (background-task tools). */ function presentTaskCall(verb: string, args: { task_id: string }): GenericCallView { return { card: 'generic', title: `${verb} background task ${args.task_id}`, kind: 'execute', rawInput: args.task_id } diff --git a/packages/bash/tool-bash/src/render.ts b/packages/bash/tool-bash/src/render.ts index f8d9398fa1..924861bb1e 100644 --- a/packages/bash/tool-bash/src/render.ts +++ b/packages/bash/tool-bash/src/render.ts @@ -68,3 +68,25 @@ export function renderResult( if (!body.endsWith('\n')) body += '\n' return body + markers.join('\n') } + +/** + * Recover the structured exit status from a rendered {@link renderResult} + * string — the inverse of the status markers it appends. A killed marker + * yields `signal`; otherwise a non-zero marker yields `exitCode`; absent both + * means a clean exit 0. + * + * Replay only retains the rendered content text, not the original + * `BashRunResult`, so terminal presentation must recover the exit pill here. + * Requiring a leading newline and the end of the string keeps ordinary output + * that merely ends with marker-like text from matching unless the final line + * is indistinguishable from a real marker. + * @param text - rendered model-facing bash result. + * @returns the recovered terminal exit code or signal. + */ +export function parseExitStatus(text: string): { exitCode: number } | { signal: string } { + const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text) + if (signal?.[1] !== undefined) return { signal: signal[1] } + const exit = /\n\[exit code: (\d+)\]$/.exec(text) + if (exit?.[1] !== undefined) return { exitCode: Number(exit[1]) } + return { exitCode: 0 } +} From c3148d46d5e17e6b2071950d9ab3d1f1cc6367dd Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:52:06 +0800 Subject: [PATCH 05/21] refactor: narrow workflow worker surface --- docs/config-catalog.md | 2 +- packages/workflow/workflow-workerthread/README.md | 2 ++ packages/workflow/workflow-workerthread/src/index.ts | 6 +----- .../tests/workflow-workerthread.spec.ts | 4 +++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9d4b8038b5..4902387649 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1166,7 +1166,7 @@ export interface Config { } ``` -Source: [`packages/workflow/workflow-workerthread/src/index.ts:69`](../packages/workflow/workflow-workerthread/src/index.ts) +Source: [`packages/workflow/workflow-workerthread/src/index.ts:65`](../packages/workflow/workflow-workerthread/src/index.ts) ## Loadable plugins with no config diff --git a/packages/workflow/workflow-workerthread/README.md b/packages/workflow/workflow-workerthread/README.md index 023b562b42..2dbac48163 100644 --- a/packages/workflow/workflow-workerthread/README.md +++ b/packages/workflow/workflow-workerthread/README.md @@ -2,6 +2,8 @@ This package implements `WorkflowService` with one Node worker thread per run. The worker executes the orchestration script; child agents remain on the host and are reached through `ctx.subagents` over a typed host/worker protocol. +The package root exports the default engine plugin and its `Config`; the worker protocol, runtime, and session modules stay private to the implementation. The operational `./worker` entry remains the engine's spawn target. + The split has one primary purpose: a synchronous script loop cannot block the harness event loop, and a script that ignores cancellation can be terminated with its worker. It is not a security sandbox. ## Trust and isolation boundary diff --git a/packages/workflow/workflow-workerthread/src/index.ts b/packages/workflow/workflow-workerthread/src/index.ts index a1f5b47ccc..e6643da09e 100644 --- a/packages/workflow/workflow-workerthread/src/index.ts +++ b/packages/workflow/workflow-workerthread/src/index.ts @@ -51,11 +51,7 @@ import { validateMeta } from './meta.ts' import type { WorkerInit, WorkerLimits } from './types.ts' export { validateMeta } from './meta.ts' -export { HostToWorkerType, WorkerToHostType } from './protocol.ts' -export type { HostToWorkerMessage, HostToWorkerPayloads, WorkerToHostMessage, WorkerToHostPayloads } from './protocol.ts' export { materializeFromRealm, MaterializeError } from './realm.ts' -export { WorkflowExecution, type ExecutionObserver } from './runtime.ts' -export { requireParentPort, runWorkerSession } from './session.ts' export type { ChildHandle, ChildPort, @@ -116,7 +112,7 @@ function assertBodyParses(body: string, name: string): void { * `result` never rejects; the `workflow/*` events fire around the run per * the seam contract. */ -export class WorkerWorkflowEngine extends WorkflowService { +class WorkerWorkflowEngine extends WorkflowService { static inject = ['subagents'] static Config: z = z.object({ diff --git a/packages/workflow/workflow-workerthread/tests/workflow-workerthread.spec.ts b/packages/workflow/workflow-workerthread/tests/workflow-workerthread.spec.ts index 4ad00ee02f..245f1c3101 100644 --- a/packages/workflow/workflow-workerthread/tests/workflow-workerthread.spec.ts +++ b/packages/workflow/workflow-workerthread/tests/workflow-workerthread.spec.ts @@ -9,7 +9,8 @@ import SubagentService from '@deepseek-ai/dsh-subagent' import type { SubagentCapabilities, SubagentProvider, SubagentResult, SubagentRun, SubagentStartRequest } from '@deepseek-ai/dsh-subagent' import type { WorkflowMeta, WorkflowResult, WorkflowResultInfo, WorkflowRunInfo } from '@deepseek-ai/dsh-workflow' import * as workerEngineModule from '../src/index.ts' -import WorkerWorkflowEngine, { HostToWorkerType, WorkerToHostType, type Config } from '../src/index.ts' +import WorkerWorkflowEngine, { type Config } from '../src/index.ts' +import { HostToWorkerType, WorkerToHostType } from '../src/protocol.ts' /** A minimal parent stand-in: the engine only threads it through to the provider. */ function fakeParent(): Agent { @@ -1340,6 +1341,7 @@ describe('dsh-workflow-workerthread', () => { it('has the class-plugin export shape (default = the engine service class)', () => { expect(workerEngineModule.default).toBe(WorkerWorkflowEngine) + expect('WorkerWorkflowEngine' in workerEngineModule).toBe(false) const loader = Object.create(Loader.prototype) as Loader const unwrapped: unknown = loader.unwrapExports(workerEngineModule) expect(unwrapped).toBe(WorkerWorkflowEngine) From 3ab35de64f830bda324695cb5ef12cf2269fb835 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 04:17:38 +0800 Subject: [PATCH 06/21] refactor: prune unused web seam fields --- docs/config-catalog.md | 4 +- docs/cordis-catalog/services.md | 8 +-- docs/core-data-structures/core.md | 2 +- docs/core-data-structures/web.md | 16 +---- .../2026-06-24-web-capability-seam.md | 45 ++++-------- .../2026-07-07-tool-call-timeout-policy.md | 2 +- ...drop-unconsumed-web-observation-surface.md | 2 +- .../cordis/tool-cordis/src/api-catalog.ts | 22 ++---- packages/web/tool-web/README.md | 2 +- packages/web/tool-web/src/fetch.ts | 2 +- packages/web/tool-web/src/search.ts | 2 +- .../web/tool-web/tests/integration.spec.ts | 20 ++++-- packages/web/tool-web/tests/tool-web.spec.ts | 68 +++++++++---------- packages/web/web-fetch-local/README.md | 5 +- packages/web/web-fetch-local/src/index.ts | 5 -- packages/web/web-fetch-local/src/provider.ts | 18 ++--- .../web-fetch-local/tests/fetch-local.spec.ts | 16 ++--- packages/web/web-search-deepseek/README.md | 4 +- .../web/web-search-deepseek/src/provider.ts | 24 +++---- .../web-search-deepseek/tests/deepseek.e2e.ts | 1 - .../tests/deepseek.spec.ts | 43 +++++------- packages/web/web-search-exa/README.md | 4 +- packages/web/web-search-exa/src/provider.ts | 25 +++---- packages/web/web-search-exa/tests/exa.e2e.ts | 1 - packages/web/web-search-exa/tests/exa.spec.ts | 29 +++----- packages/web/web-search-perplexity/README.md | 4 +- .../web/web-search-perplexity/src/provider.ts | 23 +++---- .../tests/perplexity.e2e.ts | 1 - .../tests/perplexity.spec.ts | 36 ++++------ packages/web/web/README.md | 10 +-- packages/web/web/src/index.ts | 24 +++---- packages/web/web/src/types.ts | 61 ++++------------- packages/web/web/tests/web.spec.ts | 42 ++++++------ scripts/type-equiv.manifest.json | 1 - 34 files changed, 228 insertions(+), 344 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9d4b8038b5..8dec62dce6 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1044,7 +1044,7 @@ export interface WebServiceConfig { } ``` -Source: [`packages/web/web/src/index.ts:68`](../packages/web/web/src/index.ts) +Source: [`packages/web/web/src/index.ts:64`](../packages/web/web/src/index.ts) ## `@deepseek-ai/dsh-web-fetch-local` @@ -1061,8 +1061,6 @@ export interface Config { maxBodyChars?: number /** Default fetch timeout in milliseconds. */ timeoutMs?: number - /** Upper bound for a per-request timeout override. */ - maxTimeoutMs?: number /** Maximum number of same-origin redirect hops to follow. */ maxRedirects?: number /** `User-Agent` header sent on every request. */ diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 0aea3f1589..a65fc16633 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -295,7 +295,7 @@ The web access service. Registered as `ctx.web` (one instance per context). Selection semantics (resolved at execution time, never order-dependent): -- A configured id that is registered and `status().available` → that provider. +- A configured id that is registered and `available()` → that provider. - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`. - A configured id registered but unavailable → `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. - No id configured, exactly one registered usable provider → that provider. @@ -305,11 +305,11 @@ Selection semantics (resolved at execution time, never order-dependent): ```ts cordis-catalog registerSearchProvider(provider: WebSearchProvider): () => void registerFetchProvider(provider: WebFetchProvider): () => void -async search(request: WebSearchRequest, exec?: WebExecContext): Promise -async fetch(request: WebFetchRequest, exec?: WebExecContext): Promise +async search(request: WebSearchRequest, signal?: AbortSignal): Promise +async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise ``` -Source: [`packages/web/web/src/index.ts:87`](../../packages/web/web/src/index.ts) +Source: [`packages/web/web/src/index.ts:83`](../../packages/web/web/src/index.ts) ## `ctx.workflows` — `WorkflowService` (abstract seam) diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 757c74f399..9f94036cdc 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -30,7 +30,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t | [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, model-facing `skill` loading | | [compaction.md](compaction.md) | the compaction seam: the `compact/*` session events, `CompactionResult`, the `CompactService` interface | | [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split | -| [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider/capability status, `WebError` | +| [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` | | [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality | > Type definitions on this page are pasted **verbatim** from source and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Inline JSDoc is omitted for readability; follow the source link for the full contracts. diff --git a/docs/core-data-structures/web.md b/docs/core-data-structures/web.md index f6bd6406ab..204f78dbf1 100644 --- a/docs/core-data-structures/web.md +++ b/docs/core-data-structures/web.md @@ -25,8 +25,6 @@ interface WebSearchRequest { ```ts type-equiv interface WebSearchResult { - readonly providerId: string - readonly query: string readonly content?: string readonly sources: readonly WebSearchSource[] readonly truncated: boolean @@ -49,7 +47,6 @@ interface WebSearchSource { ```ts type-equiv interface WebFetchRequest { readonly url: string - readonly timeoutMs?: number } ``` @@ -57,7 +54,6 @@ HTTP status is part of the fetched resource state, not automatically a failure: ```ts type-equiv interface WebFetchResult { - readonly providerId: string readonly url: string readonly statusCode: number readonly body: WebFetchBody @@ -73,15 +69,9 @@ type WebFetchBody = | { readonly kind: 'text'; readonly content: string } ``` -## Provider status +## Provider availability -A provider's `status()` is a cheap LOCAL check (credential presence, parseable config) and **must not make network calls**. It is an input to execution-time selection, not a health system: `search()`/`fetch()` read it to pick a usable provider, and a selection failure surfaces as the structured `WebError` the caller routes on — which carries the branchable detail (the missing id, the ambiguous candidate set) in its code and message. - -```ts type-equiv -type WebProviderStatus = - | { readonly available: true } - | { readonly available: false; readonly reason: 'missing-credential' | 'misconfigured' } -``` +A provider's `available(): boolean` is a cheap LOCAL check (credential presence, parseable config) and **must not make network calls**. It is an input to execution-time selection, not a health system: `search()`/`fetch()` read it to pick a usable provider, and a selection failure surfaces as the structured `WebError` the caller routes on — which carries the branchable detail (the missing id or ambiguous candidate set) in its code and message. Selection never depends on registration, config, or HMR order: a capability has an explicit provider id (config `searchProvider`/`fetchProvider`, or the matching env var feeding the same field), or auto-selects when exactly one usable provider is registered; multiple usable providers with no configured id is `WEB_PROVIDER_AMBIGUOUS`, not first-wins. @@ -91,4 +81,4 @@ Selection never depends on registration, config, or HMR order: a capability has ## The service -`WebService` (`ctx.web`, defined in [`packages/web/web/src/index.ts`](../../packages/web/web/src/index.ts)) is a provider registry plus a provider-selecting execution surface, close to `LlmService`'s shape: `registerSearchProvider`/`registerFetchProvider` (duplicate ids throw `WEB_DUPLICATE_PROVIDER`, return disposers) and `search`/`fetch` (resolve the provider at call time, throw a structured `WebError` when the capability cannot run). Providers issue requests with platform-native `fetch` at the repo's Node floor, mirroring `dsh-llm-deepseek`; the `dsh-web-fetch-local` provider owns safe retrieval (http/https-only, credential rejection, byte/char/timeout/redirect caps, same-origin-only redirects with per-hop re-validation, charset decoding) while `dsh-tool-web` owns presentation (HTML→markdown). SSRF / private-network blocking is deferred (see the RFC) — until it lands, `web_fetch` must not be enabled where it can reach sensitive internal targets. +`WebService` (`ctx.web`, defined in [`packages/web/web/src/index.ts`](../../packages/web/web/src/index.ts)) is a provider registry plus a provider-selecting execution surface, close to `LlmService`'s shape: `registerSearchProvider`/`registerFetchProvider` (duplicate ids throw `WEB_DUPLICATE_PROVIDER`, return disposers) and `search`/`fetch` (resolve the provider at call time, accept a direct optional cancellation signal, and throw a structured `WebError` when the capability cannot run). Providers issue requests with platform-native `fetch` at the repo's Node floor, mirroring `dsh-llm-deepseek`; the `dsh-web-fetch-local` provider owns safe retrieval (http/https-only, credential rejection, byte/char/timeout/redirect caps, same-origin-only redirects with per-hop re-validation, charset decoding) while `dsh-tool-web` owns presentation (HTML→markdown). SSRF / private-network blocking is deferred (see the RFC) — until it lands, `web_fetch` must not be enabled where it can reach sensitive internal targets. diff --git a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md b/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md index 660b9821ff..0763a771f5 100644 --- a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md +++ b/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md @@ -77,52 +77,42 @@ Provider packages depend on `@deepseek-ai/dsh-web` and Cordis. They own credenti ```ts interface WebSearchProvider { readonly id: string - status(): WebProviderStatus - search(request: WebSearchRequest, exec?: WebExecContext): Promise + available(): boolean + search(request: WebSearchRequest, signal?: AbortSignal): Promise } interface WebFetchProvider { readonly id: string - status(): WebProviderStatus - fetch(request: WebFetchRequest, exec?: WebExecContext): Promise + available(): boolean + fetch(request: WebFetchRequest, signal?: AbortSignal): Promise } interface WebService { registerSearchProvider(provider: WebSearchProvider): () => void registerFetchProvider(provider: WebFetchProvider): () => void - search(request: WebSearchRequest, exec?: WebExecContext): Promise - fetch(request: WebFetchRequest, exec?: WebExecContext): Promise -} - -interface WebExecContext { - readonly signal?: AbortSignal + search(request: WebSearchRequest, signal?: AbortSignal): Promise + fetch(request: WebFetchRequest, signal?: AbortSignal): Promise } ``` -`WebExecContext` is execution control, not business input. It carries only `signal`, so `tool-web` propagates turn cancellation, tool timeout, and agent disposal into provider network requests, SSE readers, and expensive decoding. It does not pass `ToolExecution` through the seam — that would make `dsh-web` depend on `dsh-tools`. +The optional signal is execution control, not business input: `tool-web` passes `exec.signal` directly so turn cancellation, tool timeout, and agent disposal reach provider network requests, stream readers, and expensive decoding. The seam does not pass `ToolExecution` through — that would make `dsh-web` depend on `dsh-tools`. Provider ids are stable strings and unique within their capability kind. Registering a duplicate search provider id or duplicate fetch provider id fails rather than silently replacing the old provider. Provider registration returns a disposer and follows the existing `ctx.tools.register()` / `ctx.systemPrompt.section()` pattern: the mutation is wrapped in `ctx.effect()` so the registration is torn down with the contributing fiber. -## Provider status and selection +## Provider availability and selection -Provider status and capability selection are separate concepts, but both stay minimal. A provider reports only whether that concrete implementation is usable by cheap local checks such as credential presence or parseable endpoint config. A provider `status()` must not make network calls. +Provider availability and capability selection are separate concepts, but both stay minimal. A provider reports only whether that concrete implementation is usable by cheap local checks such as credential presence or parseable endpoint config. A provider `available()` must not make network calls. -`LlmService` has no status type at all: availability is expressed as registry membership plus a resolution-time throw. `ctx.web` follows the same discipline. The seam exposes no aggregated capability-status query — `search()` / `fetch()` derive the selection on each call from the configured provider id, the registered providers, and each provider's cheap local `status()`, and a selection failure is the structured `WebError` thrown at execution time, whose code answers "in which broad category does this capability fail" and whose message answers "exactly which provider/ids/reason." A caller that needs to know whether a capability can run executes and routes that error; nothing is stored as mutable service state. +`LlmService` has no status type at all: availability is expressed as registry membership plus a resolution-time throw. `ctx.web` follows the same discipline. The seam exposes no aggregated capability-status query — `search()` / `fetch()` derive the selection on each call from the configured provider id, the registered providers, and each provider's cheap local `available()` boolean, and a selection failure is the structured `WebError` thrown at execution time. A caller that needs to know whether a capability can run executes and routes that error; nothing is stored as mutable service state. -`WebProviderStatus` is an input to selection, not a health system. `tool-web` never calls a provider's `status()` directly — its only path into the seam is `search()` / `fetch()` — so selection policy has one owner. - -```ts -type WebProviderStatus = - | { readonly available: true } - | { readonly available: false; readonly reason: 'missing-credential' | 'misconfigured' } -``` +The boolean is an input to selection, not a health system. `tool-web` never calls a provider's `available()` directly — its only path into the seam is `search()` / `fetch()` — so selection policy has one owner. Selection must not depend on registration order. Cordis load order, config ordering, and HMR timing are not product semantics. | Situation | Execution behavior | |---|---| -| A configured provider id is registered and `status().available === true` | runs that provider | +| A configured provider id is registered and `available() === true` | runs that provider | | A configured provider id is not registered | fails with `WEB_PROVIDER_CONFIGURED_MISSING` | | A configured provider id is registered but unavailable | fails with `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` | | No provider id is configured and exactly one provider for that kind is registered and available | runs that single provider | @@ -184,8 +174,6 @@ interface WebSearchRequest { } interface WebSearchResult { - readonly providerId: string - readonly query: string readonly content?: string readonly sources: readonly WebSearchSource[] readonly truncated: boolean @@ -212,20 +200,17 @@ The `web_fetch` implementation is an anonymous public HTTP(S) fetch provider, `l The seam request stays smaller than OpenCode's model-facing tool: - `url`: required HTTP(S) URL. -- `timeoutMs`: optional positive number capped by the provider. -The seam request deliberately does not include `format`, `prompt`, or provider-specific extraction controls. `format` is a presentation decision over a fetched resource; `prompt` is a higher-level LLM summarization instruction; extraction APIs such as Firecrawl, Exa, Tavily, or Parallel may not expose a concrete HTTP response. If the product later needs provider-backed page extraction, that is a separate `web_extract` capability or a deliberate widening of this seam — extract semantics are never smuggled into `web_fetch` by making every HTTP field optional. +The seam request deliberately does not include a per-call timeout, `format`, `prompt`, or provider-specific extraction controls. Cancellation is the direct optional execution signal, while the fetch provider owns one deployment-configured timeout backstop. `format` is a presentation decision over a fetched resource; `prompt` is a higher-level LLM summarization instruction; extraction APIs such as Firecrawl, Exa, Tavily, or Parallel may not expose a concrete HTTP response. If the product later needs provider-backed page extraction, that is a separate `web_extract` capability or a deliberate widening of this seam — extract semantics are never smuggled into `web_fetch` by making every HTTP field optional. HTTP status is part of the fetched resource state, not automatically a tool failure. A successful network fetch of a `404` or `500` response returns `WebFetchResult` with the status code and a bounded decoded body when the content type is supported. `WebError` is for failures to safely retrieve or represent the resource: invalid or blocked URL, redirect policy violation, timeout, abort, response too large, unsupported content type, provider failure, or network failure. ```ts interface WebFetchRequest { readonly url: string - readonly timeoutMs?: number } interface WebFetchResult { - readonly providerId: string readonly url: string readonly statusCode: number readonly body: WebFetchBody @@ -257,11 +242,11 @@ SSRF / private-network protection (blocking private, loopback, link-local, multi `dsh-tool-web` owns two `ToolDefinition`s: `web_search` and `web_fetch`. It owns model-facing JSON schemas, snake_case argument names, prompt sections, result rendering to `ContentBlock[]`, `presentCall`, and `presentResult`. -`dsh-tool-web` must not enumerate providers or call provider `status()` directly. Its only path into the seam is `ctx.web.search()` / `ctx.web.fetch()`. That keeps provider selection in one layer; otherwise the tool package could decide one provider is usable while execution resolves a different state. +`dsh-tool-web` must not enumerate providers or call provider `available()` directly. Its only path into the seam is `ctx.web.search()` / `ctx.web.fetch()`. That keeps provider selection in one layer; otherwise the tool package could decide one provider is usable while execution resolves a different state. Tool registration is a minimal stable sync: on plugin startup the `dsh-tool-web` `Config` (`search?: boolean`, `fetch?: boolean`, both default `true`) enables or disables each web tool; an enabled tool is registered with a fiber-scoped disposer via the effect-based registry; neither tool is disposed merely because its selected provider is missing, unusable, or ambiguous; disposing the `tool-web` fiber tears down its registrations automatically. -Provider status changes affect execution results and diagnostics, not whether the model-facing schema exists. If a product wants no web tools at all, it disables `dsh-tool-web` or the individual web tool in config; if it wants web tools but the backend is misconfigured, the model sees a structured tool error at execution time. +Provider availability changes affect execution results and diagnostics, not whether the model-facing schema exists. If a product wants no web tools at all, it disables `dsh-tool-web` or the individual web tool in config; if it wants web tools but the backend is misconfigured, the model sees a structured tool error at execution time. The prompt guidance explains the semantic split — `web_search` for discovery and current information, `web_fetch` when the model needs the content of a specific URL — and the prompt and tool result tell the model to cite relevant URLs with markdown links. diff --git a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md index 362f5cb5e8..1ce2e9dde6 100644 --- a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md +++ b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md @@ -75,7 +75,7 @@ No new session event is needed for reconstructability: `TOOL_TIMEOUT` is the fin `web_fetch` and `web_search` are migrated. `dsh-tool-web` keeps ownership of their model-facing schemas, and those schemas expose no timeout knob: `web_fetch` dropped its `timeout_ms` parameter to match the reference-agent shape, and `web_search` stays query-only. The tool bodies do not import `@deepseek-ai/dsh-timeout`; they forward `exec.signal` to `ctx.web`. -`dsh-web-fetch-local` keeps a provider-level timeout (`timeoutMs`/`maxTimeoutMs`) as a large resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments; it owns no model-facing timeout. When a `TOOL_TIMEOUT` signal reaches the fetch provider first, provider-scoped classification treats it as upstream `WEB_ABORTED`, and the outer `tools/execute` wrapper replaces the final tool result with `TOOL_TIMEOUT`. A shipped web-tool deployment configures the provider backstop above the `timeout-policy` budget so the tool-call policy normally wins for model calls. +`dsh-web-fetch-local` keeps one configured provider-level `timeoutMs` as a large resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments; it owns no model-facing timeout. When a `TOOL_TIMEOUT` signal reaches the fetch provider first, provider-scoped classification treats it as upstream `WEB_ABORTED`, and the outer `tools/execute` wrapper replaces the final tool result with `TOOL_TIMEOUT`. A shipped web-tool deployment configures the provider backstop above the `timeout-policy` budget so the tool-call policy normally wins for model calls. `bash` stays on the current backend timeout path. `dsh-tool-bash` continues to expose `timeoutMs` and `run_in_background`; `dsh-bash-local` continues to use `@deepseek-ai/dsh-timeout` for `BASH_TIMEOUT`; hook bridges continue to call `runHook()` and pass `timeoutMs` through `ctx.bash`. This keeps foreground/background/hook behavior stable. diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md b/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md index b6fbded6ac..7f8bf80d97 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md +++ b/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md @@ -15,7 +15,7 @@ This mirrors [drop the unconsumed `llm/adapter-change` event](../../implemented/ ## Decision -The event declaration, both emits, and the rollback-before-emit ordering are deleted (the plain `ctx.effect` disposer carries HMR cleanup). `searchStatus()`/`fetchStatus()`/`WebCapabilityStatus` are deleted — the provider-private `status()` stays, since it feeds execution-time selection. The listener-throw rollback test that existed solely for the removed event is gone, and the emission assertions and every status-based assertion are rewritten onto the behavior a real caller observes: a successful `search()`/`fetch()`, or the structured `WebError` codes for unavailable/ambiguous/misconfigured provider sets. The cordis catalog is regenerated; `packages/web/web/README.md`, `packages/web/tool-web/README.md` (the drifted reads-status sentence), [web.md](../../../core-data-structures/web.md), and the web paragraph in [architecture.md](../../../architecture.md) describe the shipped contract; the [web capability seam RFC](../../implemented/architecture/2026-06-24-web-capability-seam.md)'s facts (it specified the event and the status aggregation) are amended per [implemented/AGENTS.md](../AGENTS.md). +The event declaration, both emits, and the rollback-before-emit ordering are deleted (the plain `ctx.effect` disposer carries HMR cleanup). `searchStatus()`/`fetchStatus()`/`WebCapabilityStatus` are deleted — the provider-private availability check stays, since it feeds execution-time selection. The listener-throw rollback test that existed solely for the removed event is gone, and the emission assertions and every status-based assertion are rewritten onto the behavior a real caller observes: a successful `search()`/`fetch()`, or the structured `WebError` codes for unavailable/ambiguous/misconfigured provider sets. The cordis catalog is regenerated; `packages/web/web/README.md`, `packages/web/tool-web/README.md` (the drifted reads-status sentence), [web.md](../../../core-data-structures/web.md), and the web paragraph in [architecture.md](../../../architecture.md) describe the shipped contract; the [web capability seam RFC](../../implemented/architecture/2026-06-24-web-capability-seam.md)'s facts (it specified the event and the status aggregation) are amended per [implemented/AGENTS.md](../AGENTS.md). ## Alternatives considered diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index b4420e80c7..013cadcebc 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -221,8 +221,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ methods: [ 'registerSearchProvider(provider: WebSearchProvider): () => void', 'registerFetchProvider(provider: WebFetchProvider): () => void', - 'async search(request: WebSearchRequest, exec?: WebExecContext): Promise', - 'async fetch(request: WebFetchRequest, exec?: WebExecContext): Promise', + 'async search(request: WebSearchRequest, signal?: AbortSignal): Promise', + 'async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise', ], }, { @@ -994,33 +994,25 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'UserInteractionProvider', declaration: 'export interface UserInteractionProvider {\n ask(request: AskUserQuestionRequest): Promise;\n}', }, - { - name: 'WebExecContext', - declaration: 'export interface WebExecContext {\n readonly signal?: AbortSignal;\n}', - }, { name: 'WebFetchBody', declaration: 'export type WebFetchBody = {\n readonly kind: \'html\';\n readonly content: string;\n} | {\n readonly kind: \'text\';\n readonly content: string;\n};', }, { name: 'WebFetchProvider', - declaration: 'export interface WebFetchProvider {\n readonly id: string;\n status(): WebProviderStatus;\n fetch(request: WebFetchRequest, exec?: WebExecContext): Promise;\n}', + declaration: 'export interface WebFetchProvider {\n readonly id: string;\n available(): boolean;\n fetch(request: WebFetchRequest, signal?: AbortSignal): Promise;\n}', }, { name: 'WebFetchRequest', - declaration: 'export interface WebFetchRequest {\n readonly url: string;\n readonly timeoutMs?: number;\n}', + declaration: 'export interface WebFetchRequest {\n readonly url: string;\n}', }, { name: 'WebFetchResult', - declaration: 'export interface WebFetchResult {\n readonly providerId: string;\n readonly url: string;\n readonly statusCode: number;\n readonly body: WebFetchBody;\n readonly truncated: boolean;\n}', - }, - { - name: 'WebProviderStatus', - declaration: 'export type WebProviderStatus = {\n readonly available: true;\n} | {\n readonly available: false;\n readonly reason: \'missing-credential\' | \'misconfigured\';\n};', + declaration: 'export interface WebFetchResult {\n readonly url: string;\n readonly statusCode: number;\n readonly body: WebFetchBody;\n readonly truncated: boolean;\n}', }, { name: 'WebSearchProvider', - declaration: 'export interface WebSearchProvider {\n readonly id: string;\n status(): WebProviderStatus;\n search(request: WebSearchRequest, exec?: WebExecContext): Promise;\n}', + declaration: 'export interface WebSearchProvider {\n readonly id: string;\n available(): boolean;\n search(request: WebSearchRequest, signal?: AbortSignal): Promise;\n}', }, { name: 'WebSearchRequest', @@ -1028,7 +1020,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'WebSearchResult', - declaration: 'export interface WebSearchResult {\n readonly providerId: string;\n readonly query: string;\n readonly content?: string;\n readonly sources: readonly WebSearchSource[];\n readonly truncated: boolean;\n}', + declaration: 'export interface WebSearchResult {\n readonly content?: string;\n readonly sources: readonly WebSearchSource[];\n readonly truncated: boolean;\n}', }, { name: 'WebSearchSource', diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md index ab1326a21e..8ae9485aab 100644 --- a/packages/web/tool-web/README.md +++ b/packages/web/tool-web/README.md @@ -32,4 +32,4 @@ Each tool is registered independently; a product that wants only one disables th Tool registration follows product **enablement**, not backend availability. A tool stays visible even when its selected provider is missing, misconfigured, ambiguous, or temporarily unavailable; the seam resolves the provider at execution time and execution fails with a structured `WebError` (e.g. `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_AMBIGUOUS`), which `ToolRegistry.execute()` turns into an error tool result the model can read and hooks/UI can route on. This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. To remove a web tool entirely, disable it here in config. -The tool never calls a provider's `status()` and never enumerates providers — its only execution path is `ctx.web.search()` / `ctx.web.fetch()`, and provider unavailability reaches it as the structured `WebError` codes selection throws at execution time. Provider selection stays entirely inside the seam, with one owner. +The tool never calls a provider's `available()` and never enumerates providers — its only execution path is `ctx.web.search()` / `ctx.web.fetch()`, and provider unavailability reaches it as the structured `WebError` codes selection throws at execution time. Provider selection stays entirely inside the seam, with one owner. diff --git a/packages/web/tool-web/src/fetch.ts b/packages/web/tool-web/src/fetch.ts index 571ce00797..b803673bc6 100644 --- a/packages/web/tool-web/src/fetch.ts +++ b/packages/web/tool-web/src/fetch.ts @@ -103,7 +103,7 @@ export function applyWebFetchTool(ctx: Context, timeoutMs: number): void { const input = parseFetchArgs(args) const result = await ctx.web.fetch( { url: input.url }, - exec.signal ? { signal: exec.signal } : undefined, + exec.signal, ) return [{ type: 'text', text: formatFetchOutput(result) }] }, diff --git a/packages/web/tool-web/src/search.ts b/packages/web/tool-web/src/search.ts index a7587d328b..6db829b7fc 100644 --- a/packages/web/tool-web/src/search.ts +++ b/packages/web/tool-web/src/search.ts @@ -113,7 +113,7 @@ export function applyWebSearchTool(ctx: Context, maxResults: number, timeoutMs: const input = parseSearchArgs(args) const result = await ctx.web.search( { query: input.query, maxResults }, - exec.signal ? { signal: exec.signal } : undefined, + exec.signal, ) return [{ type: 'text', text: formatSearchOutput(result) }] }, diff --git a/packages/web/tool-web/tests/integration.spec.ts b/packages/web/tool-web/tests/integration.spec.ts index de804e2bcd..f46f34970f 100644 --- a/packages/web/tool-web/tests/integration.spec.ts +++ b/packages/web/tool-web/tests/integration.spec.ts @@ -136,7 +136,7 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc await tctx.plugin(ToolRegistry) await tctx.plugin(WebService, { fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID }) // Provider backstop well ABOVE the tool-call budget, so the policy wins. - await tctx.plugin(WebFetchLocal, { timeoutMs: 30_000, maxTimeoutMs: 60_000 }) + await tctx.plugin(WebFetchLocal, { timeoutMs: 30_000 }) await tctx.plugin(TimeoutPolicy) // The tool-call budget is declared by tool-web config, enforced by the policy. tfiber = await tctx.plugin(ToolWeb, { fetchTimeoutMs: 50 }) @@ -158,12 +158,20 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc expect(text).toContain('timed out after 50ms') }) - it('the provider backstop still protects a DIRECT ctx.web.fetch() call (no tool-call policy in that path)', async () => { - // A direct seam caller does not go through tools/execute, so the tool-call + it('the provider backstop still protects a direct provider call (no tool-call policy in that path)', async () => { + // A direct provider caller does not go through tools/execute, so the tool-call // policy never applies; the provider's OWN timeout is the only budget. A - // short per-request hint proves the provider backstop is intact and classifies - // as WEB_FETCH_TIMEOUT (the provider-owned code), never TOOL_TIMEOUT. - const err = await tctx.web.fetch({ url: slowBase, timeoutMs: 50 }).then( + // A second direct provider with a short configured backstop proves the + // provider-owned deadline remains intact and distinct from TOOL_TIMEOUT. + const direct = new WebFetchLocal.LocalFetchProvider({ + maxUrlLength: 2048, + maxResponseBytes: 5_000_000, + maxBodyChars: 100_000, + timeoutMs: 50, + maxRedirects: 5, + userAgent: 'integration-test', + }) + const err = await direct.fetch({ url: slowBase }).then( () => undefined, (e: unknown) => e as { code?: string }, ) diff --git a/packages/web/tool-web/tests/tool-web.spec.ts b/packages/web/tool-web/tests/tool-web.spec.ts index 4bb2728df7..a9f66d3746 100644 --- a/packages/web/tool-web/tests/tool-web.spec.ts +++ b/packages/web/tool-web/tests/tool-web.spec.ts @@ -4,7 +4,7 @@ import { CallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import WebService from '@deepseek-ai/dsh-web' -import type { WebSearchProvider, WebSearchResult, WebProviderStatus } from '@deepseek-ai/dsh-web' +import type { WebSearchProvider, WebSearchResult } from '@deepseek-ai/dsh-web' import * as ToolWeb from '@deepseek-ai/dsh-tool-web' import { formatSearchOutput, @@ -18,10 +18,10 @@ import { WEB_SEARCH_MAX_RESULTS, } from '@deepseek-ai/dsh-tool-web' -const available: WebProviderStatus = { available: true } +const available = true -function searchProvider(result: WebSearchResult, status: WebProviderStatus = available): WebSearchProvider { - return { id: 'stub-search', status: () => status, search: () => Promise.resolve(result) } +function searchProvider(result: WebSearchResult, isAvailable = available): WebSearchProvider { + return { id: 'stub-search', available: () => isAvailable, search: () => Promise.resolve(result) } } /** Mount the real registry, seam, and tool-web; return an executor helper. */ @@ -46,7 +46,7 @@ async function mountTools(opts: { describe('search formatting', () => { it('renders content, sources with titles/hostnames, snippets, and a citation reminder', () => { const out = formatSearchOutput({ - providerId: 'p', query: 'q', content: 'an answer', truncated: false, + content: 'an answer', truncated: false, sources: [ { url: 'https://a.test/x', title: 'A', snippet: 'about a', publishedAt: '2026-01-01' }, { url: 'https://b.test/y' }, @@ -59,19 +59,19 @@ describe('search formatting', () => { }) it('reports no results when there is neither content nor sources', () => { - expect(formatSearchOutput({ providerId: 'p', query: 'q', sources: [], truncated: false })) + expect(formatSearchOutput({ sources: [], truncated: false })) .toContain('No results found.') }) it('renders content alone when there are no sources', () => { - const out = formatSearchOutput({ providerId: 'p', query: 'q', content: 'just an answer', sources: [], truncated: false }) + const out = formatSearchOutput({ content: 'just an answer', sources: [], truncated: false }) expect(out).toContain('just an answer') expect(out).not.toContain('No results found.') expect(out).not.toContain('Sources:') }) it('notes truncation', () => { - const out = formatSearchOutput({ providerId: 'p', query: 'q', sources: [{ url: 'https://a.test' }], truncated: true }) + const out = formatSearchOutput({ sources: [{ url: 'https://a.test' }], truncated: true }) expect(out).toContain('Showing the first 1 sources') }) @@ -88,7 +88,7 @@ describe('search formatting', () => { describe('fetch formatting', () => { it('renders an html body to markdown text with a status header', () => { const out = formatFetchOutput({ - providerId: 'p', url: 'https://a.test', statusCode: 200, truncated: false, + url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'html', content: '

Title

Body text

' }, }) expect(out).toContain('Fetched https://a.test (HTTP 200)') @@ -98,7 +98,7 @@ describe('fetch formatting', () => { it('passes a text body through and notes truncation', () => { const out = formatFetchOutput({ - providerId: 'p', url: 'https://a.test', statusCode: 200, truncated: true, + url: 'https://a.test', statusCode: 200, truncated: true, body: { kind: 'text', content: 'plain' }, }) expect(out).toContain('plain') @@ -155,7 +155,7 @@ describe('htmlToMarkdown', () => { }) it('falls back to the raw URL as a source label when the URL is unparseable', () => { - const out = formatSearchOutput({ providerId: 'p', query: 'q', truncated: false, sources: [{ url: 'not a url' }] }) + const out = formatSearchOutput({ truncated: false, sources: [{ url: 'not a url' }] }) expect(out).toContain('[not a url](not a url)') }) }) @@ -209,7 +209,7 @@ describe('tool-web registration', () => { describe('tool-web execution through the real registry', () => { it('executes web_search and formats the result', async () => { const result: WebSearchResult = { - providerId: 'stub-search', query: 'q', content: 'answer', truncated: false, + content: 'answer', truncated: false, sources: [{ url: 'https://a.test', title: 'A', snippet: 'snip' }], } const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: searchProvider(result) }) @@ -228,8 +228,8 @@ describe('tool-web execution through the real registry', () => { }) it('surfaces WEB_PROVIDER_AMBIGUOUS for multiple unconfigured providers', async () => { - const { ctx, fiber, call } = await mountTools({ search: searchProvider({ providerId: 'stub-search', query: 'q', sources: [], truncated: false }) }) - ctx.web.registerSearchProvider({ id: 'other', status: () => available, search: () => Promise.resolve({ providerId: 'other', query: 'q', sources: [], truncated: false }) }) + const { ctx, fiber, call } = await mountTools({ search: searchProvider({ sources: [], truncated: false }) }) + ctx.web.registerSearchProvider({ id: 'other', available: () => available, search: () => Promise.resolve({ sources: [], truncated: false }) }) const out = await call('web_search', { query: 'q' }) expect(out.isError).toBe(true) expect(out.error?.code).toBe('WEB_PROVIDER_AMBIGUOUS') @@ -237,7 +237,7 @@ describe('tool-web execution through the real registry', () => { }) it('rejects invalid arguments with a structured INVALID_ARGS error', async () => { - const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: searchProvider({ providerId: 'stub-search', query: 'q', sources: [], truncated: false }) }) + const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: searchProvider({ sources: [], truncated: false }) }) const out = await call('web_search', { query: 123 }) expect(out.isError).toBe(true) expect(out.error?.code).toBe('INVALID_ARGS') @@ -249,14 +249,14 @@ describe('tool-web execution through the real registry', () => { }) it('executes web_fetch, forwarding the url (no timeout param) and the abort signal to the seam', async () => { - const seen: { request?: { url: string; timeoutMs?: number }; signal?: AbortSignal | undefined } = {} + const seen: { request?: { url: string }; signal?: AbortSignal | undefined } = {} const fetchProvider = { id: 'stub-fetch', - status: () => available, - fetch: (request: { url: string; timeoutMs?: number }, exec?: { signal?: AbortSignal }) => { + available: () => available, + fetch: (request: { url: string }, signal?: AbortSignal) => { seen.request = request - seen.signal = exec?.signal - return Promise.resolve({ providerId: 'stub-fetch', url: request.url, statusCode: 200, body: { kind: 'text' as const, content: 'ok' }, truncated: false }) + seen.signal = signal + return Promise.resolve({ url: request.url, statusCode: 200, body: { kind: 'text' as const, content: 'ok' }, truncated: false }) }, } const { ctx, fiber } = await mountTools({ webConfig: { fetchProvider: 'stub-fetch' }, fetchProvider }) @@ -271,21 +271,21 @@ describe('tool-web execution through the real registry', () => { }) it('executes web_fetch with no caller signal (forwards undefined to the seam)', async () => { - const seen: { signal?: AbortSignal | undefined; passedExec?: boolean } = {} + const seen: { signal?: AbortSignal | undefined; passedSignal?: boolean } = {} const fetchProvider = { id: 'stub-fetch', - status: () => available, - fetch: (request: { url: string }, exec?: { signal?: AbortSignal }) => { - seen.passedExec = exec !== undefined - seen.signal = exec?.signal - return Promise.resolve({ providerId: 'stub-fetch', url: request.url, statusCode: 200, body: { kind: 'text' as const, content: 'ok' }, truncated: false }) + available: () => available, + fetch: (request: { url: string }, signal?: AbortSignal) => { + seen.passedSignal = signal !== undefined + seen.signal = signal + return Promise.resolve({ url: request.url, statusCode: 200, body: { kind: 'text' as const, content: 'ok' }, truncated: false }) }, } const { ctx, fiber } = await mountTools({ webConfig: { fetchProvider: 'stub-fetch' }, fetchProvider }) - // No signal on the execution: the tool passes `undefined` (not `{ signal: undefined }`). + // No signal on the execution: the tool passes `undefined`. const out = await ctx.tools.execute({ callId: CallId('fetch-2'), name: 'web_fetch', arguments: { url: 'https://a.test' } }) expect(out.isError).toBe(false) - expect(seen.passedExec).toBe(false) + expect(seen.passedSignal).toBe(false) expect(seen.signal).toBeUndefined() await fiber.dispose() }) @@ -294,8 +294,8 @@ describe('tool-web execution through the real registry', () => { const seen: { signal?: AbortSignal | undefined } = {} const provider: WebSearchProvider = { id: 'stub-search', - status: () => available, - search: (_request, exec) => { seen.signal = exec?.signal; return Promise.resolve({ providerId: 'stub-search', query: 'q', sources: [], truncated: false }) }, + available: () => available, + search: (_request, signal) => { seen.signal = signal; return Promise.resolve({ sources: [], truncated: false }) }, } const { ctx, fiber } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider }) const controller = new AbortController() @@ -310,8 +310,8 @@ describe('searchMaxResults is plugin config', () => { const seen: { maxResults?: number | undefined } = {} const provider: WebSearchProvider = { id: 'stub-search', - status: () => available, - search: (request) => { seen.maxResults = request.maxResults; return Promise.resolve({ providerId: 'stub-search', query: 'q', sources: [], truncated: false }) }, + available: () => available, + search: (request) => { seen.maxResults = request.maxResults; return Promise.resolve({ sources: [], truncated: false }) }, } const { fiber, call } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider }) await call('web_search', { query: 'q' }) @@ -323,8 +323,8 @@ describe('searchMaxResults is plugin config', () => { const sources = Array.from({ length: 5 }, (_, i) => ({ url: `https://s${i}.test` })) const provider: WebSearchProvider = { id: 'stub-search', - status: () => available, - search: request => Promise.resolve({ providerId: 'stub-search', query: request.query, sources, truncated: false }), + available: () => available, + search: () => Promise.resolve({ sources, truncated: false }), } const { fiber, call } = await mountTools({ config: { searchMaxResults: 2 }, webConfig: { searchProvider: 'stub-search' }, search: provider }) const out = await call('web_search', { query: 'q' }) diff --git a/packages/web/web-fetch-local/README.md b/packages/web/web-fetch-local/README.md index 9c2ef0030f..e84ca775f0 100644 --- a/packages/web/web-fetch-local/README.md +++ b/packages/web/web-fetch-local/README.md @@ -8,7 +8,7 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i The provider owns **safe resource retrieval**: URL validation, HTTP transport, redirect policy, a resource-backstop timeout, abort propagation, byte caps, charset decoding, content-type classification, and binary rejection. `@deepseek-ai/dsh-tool-web` owns **presentation** (HTML→markdown, truncation formatting). A non-2xx HTTP response is a *result* (status code + decoded body), not an error; `WebError` is reserved for failures to safely retrieve or represent the resource. -The provider's `timeoutMs`/`maxTimeoutMs` is a **resource backstop** for direct `ctx.web.fetch()` callers and misconfigured deployments — it is NOT the model-facing tool-call budget. The tool-call budget for `web_fetch` is deployment policy owned by [`@deepseek-ai/dsh-timeout-policy`](../../timeout/timeout-policy/README.md), which arms a per-call deadline on `exec.signal`. A shipped web-tool deployment sets the provider backstop **above** the `tool-timeout` budget, so the tool-call policy normally wins for model calls (returning `TOOL_TIMEOUT`); when the outer deadline signal reaches this provider first, it classifies as `WEB_ABORTED` and the outer wrapper replaces the result with `TOOL_TIMEOUT`. The provider's own `WEB_FETCH_TIMEOUT` only fires for a direct seam caller whose own budget elapsed. +The provider's configured `timeoutMs` is a **resource backstop** for direct `ctx.web.fetch()` callers and misconfigured deployments — it is NOT the model-facing tool-call budget. The tool-call budget for `web_fetch` is deployment policy owned by [`@deepseek-ai/dsh-timeout-policy`](../../timeout/timeout-policy/README.md), which arms a per-call deadline on `exec.signal`. A shipped web-tool deployment sets the provider backstop **above** the `tool-timeout` budget, so the tool-call policy normally wins for model calls (returning `TOOL_TIMEOUT`); when the outer deadline signal reaches this provider first, it classifies as `WEB_ABORTED` and the outer wrapper replaces the result with `TOOL_TIMEOUT`. The provider's own `WEB_FETCH_TIMEOUT` fires when its configured backstop elapses. ## Transport hygiene @@ -26,8 +26,7 @@ The provider's `timeoutMs`/`maxTimeoutMs` is a **resource backstop** for direct | `maxUrlLength` | `2048` | Maximum accepted request URL length. | | `maxResponseBytes` | `5_000_000` | Maximum response body size in bytes. | | `maxBodyChars` | `100_000` | Maximum decoded body length in characters. | -| `timeoutMs` | `30_000` | Default fetch timeout — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-timeout-policy`). | -| `maxTimeoutMs` | `120_000` | Upper bound for a per-request timeout override (direct callers). | +| `timeoutMs` | `30_000` | Fetch timeout — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-timeout-policy`). | | `maxRedirects` | `5` | Maximum same-origin redirect hops (`0` follows none). | | `userAgent` | `deepseek-harness/…` | `User-Agent` header. | diff --git a/packages/web/web-fetch-local/src/index.ts b/packages/web/web-fetch-local/src/index.ts index b6f97bf0d9..713de4c88a 100644 --- a/packages/web/web-fetch-local/src/index.ts +++ b/packages/web/web-fetch-local/src/index.ts @@ -40,8 +40,6 @@ export interface Config { maxBodyChars?: number /** Default fetch timeout in milliseconds. */ timeoutMs?: number - /** Upper bound for a per-request timeout override. */ - maxTimeoutMs?: number /** Maximum number of same-origin redirect hops to follow. */ maxRedirects?: number /** `User-Agent` header sent on every request. */ @@ -53,7 +51,6 @@ export const Config: z = z.object({ maxResponseBytes: z.number().default(5_000_000), maxBodyChars: z.number().default(100_000), timeoutMs: z.number().default(30_000), - maxTimeoutMs: z.number().default(120_000), maxRedirects: z.number().default(5), userAgent: z.string().default(DEFAULT_USER_AGENT), }) @@ -83,14 +80,12 @@ export function apply(ctx: Context, config: Config): void { assertPositiveFinite('maxResponseBytes', resolved.maxResponseBytes) assertPositiveFinite('maxBodyChars', resolved.maxBodyChars) assertPositiveFinite('timeoutMs', resolved.timeoutMs) - assertPositiveFinite('maxTimeoutMs', resolved.maxTimeoutMs) assertNonNegativeInteger('maxRedirects', resolved.maxRedirects) const limits: LocalFetchLimits = { maxUrlLength: resolved.maxUrlLength, maxResponseBytes: resolved.maxResponseBytes, maxBodyChars: resolved.maxBodyChars, timeoutMs: resolved.timeoutMs, - maxTimeoutMs: resolved.maxTimeoutMs, maxRedirects: resolved.maxRedirects, userAgent: resolved.userAgent, } diff --git a/packages/web/web-fetch-local/src/provider.ts b/packages/web/web-fetch-local/src/provider.ts index ed332c4508..787966dcd3 100644 --- a/packages/web/web-fetch-local/src/provider.ts +++ b/packages/web/web-fetch-local/src/provider.ts @@ -20,8 +20,8 @@ */ import { WebError } from '@deepseek-ai/dsh-web' -import type { WebFetchBody, WebFetchProvider, WebFetchRequest, WebFetchResult, WebProviderStatus } from '@deepseek-ai/dsh-web' -import { clampTimeout, deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' +import type { WebFetchBody, WebFetchProvider, WebFetchRequest, WebFetchResult } from '@deepseek-ai/dsh-web' +import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' import { classifyContentType, decoderForCharset, isSameOrigin, parseCharset, validateFetchUrl } from './policy.ts' /** Resolved provider limits (the plugin's schemastery Config supplies defaults). */ @@ -34,8 +34,6 @@ export interface LocalFetchLimits { maxBodyChars: number /** Default fetch timeout in milliseconds. */ timeoutMs: number - /** Upper bound for a per-request timeout override. */ - maxTimeoutMs: number /** Maximum number of (same-origin) redirect hops to follow. */ maxRedirects: number /** `User-Agent` header sent on every request. */ @@ -52,20 +50,19 @@ export class LocalFetchProvider implements WebFetchProvider { constructor(private readonly limits: LocalFetchLimits) {} /** No credentials to check — an anonymous public fetcher is always usable. */ - status(): WebProviderStatus { - return { available: true } + available(): boolean { + return true } - async fetch(request: WebFetchRequest, exec?: { readonly signal?: AbortSignal }): Promise { - if (exec?.signal?.aborted) throw new WebError('web fetch aborted', 'WEB_ABORTED') - const timeoutMs = clampTimeout(request.timeoutMs, this.limits.timeoutMs, this.limits.maxTimeoutMs) + async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise { + if (signal?.aborted) throw new WebError('web fetch aborted', 'WEB_ABORTED') // One deadline signal fuses the caller's abort with our own timeout, so the // network request and the streaming read both stop on either. The timeout // abort carries a TimeoutReason we recover afterward to classify the cause // (translateAbortOrNetwork), instead of hand-rolling a controller + timer + // reason-recovery dance. - using d = deadline(exec?.signal, timeoutMs, 'WEB_FETCH_TIMEOUT') + using d = deadline(signal, this.limits.timeoutMs, 'WEB_FETCH_TIMEOUT') return await this.followAndRead(request.url, d.signal) } @@ -161,7 +158,6 @@ export class LocalFetchProvider implements WebFetchProvider { const body: WebFetchBody = kind === 'html' ? { kind: 'html', content } : { kind: 'text', content } return { - providerId: this.id, url: finalUrl.toString(), statusCode: response.status, body, diff --git a/packages/web/web-fetch-local/tests/fetch-local.spec.ts b/packages/web/web-fetch-local/tests/fetch-local.spec.ts index 7d4c4e683b..e3eb7d30c7 100644 --- a/packages/web/web-fetch-local/tests/fetch-local.spec.ts +++ b/packages/web/web-fetch-local/tests/fetch-local.spec.ts @@ -12,7 +12,6 @@ const limits: LocalFetchLimits = { maxResponseBytes: 5_000_000, maxBodyChars: 100_000, timeoutMs: 5_000, - maxTimeoutMs: 10_000, maxRedirects: 5, userAgent: 'test-agent/1.0', } @@ -82,7 +81,7 @@ describe('LocalFetchProvider success', () => { it('fetches a text body', async () => { handler = (_req, res) => { res.writeHead(200, { 'content-type': 'text/plain' }); res.end('hello world') } const result = await provider().fetch({ url: base }) - expect(result.providerId).toBe(LOCAL_FETCH_PROVIDER_ID) + expect(provider().available()).toBe(true) expect(result.statusCode).toBe(200) expect(result.body).toEqual({ kind: 'text', content: 'hello world' }) expect(result.truncated).toBe(false) @@ -287,14 +286,14 @@ describe('LocalFetchProvider invalid URLs and abort', () => { it('honors a pre-aborted signal', async () => { const controller = new AbortController() controller.abort() - await expect(provider().fetch({ url: base }, { signal: controller.signal })) + await expect(provider().fetch({ url: base }, controller.signal)) .rejects.toThrow(expect.objectContaining({ code: 'WEB_ABORTED' })) }) it('aborts an in-flight fetch via the signal', async () => { handler = (_req, _res) => { /* never responds */ } const controller = new AbortController() - const promise = provider().fetch({ url: base }, { signal: controller.signal }) + const promise = provider().fetch({ url: base }, controller.signal) controller.abort() await expect(promise).rejects.toThrow(expect.objectContaining({ code: 'WEB_ABORTED' })) }) @@ -325,11 +324,6 @@ describe('LocalFetchProvider invalid URLs and abort', () => { .rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_ERROR' })) }) - it('caps the per-request timeout at maxTimeoutMs', async () => { - handler = (_req, res) => { res.writeHead(200, { 'content-type': 'text/plain' }); res.end('ok') } - const result = await provider({ maxTimeoutMs: 10_000 }).fetch({ url: base, timeoutMs: 999_999 }) - expect(result.statusCode).toBe(200) - }) }) describe('LocalFetchProvider body cancellation on error paths', () => { @@ -378,7 +372,7 @@ describe('web-fetch-local plugin registration', () => { await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID }) const fiber = await ctx.plugin(fetchPlugin, {}) await expect(ctx.web.fetch({ url: `${base}/` })) - .resolves.toMatchObject({ providerId: LOCAL_FETCH_PROVIDER_ID, statusCode: 200 }) + .resolves.toMatchObject({ statusCode: 200 }) await fiber.dispose() await expect(ctx.web.fetch({ url: `${base}/` })) .rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_MISSING' })) @@ -421,7 +415,7 @@ describe('web-fetch-local plugin registration', () => { await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID }) const fiber = await ctx.plugin(fetchPlugin, { maxRedirects: 0 }) await expect(ctx.web.fetch({ url: `${base}/` })) - .resolves.toMatchObject({ providerId: LOCAL_FETCH_PROVIDER_ID, statusCode: 200 }) + .resolves.toMatchObject({ statusCode: 200 }) await fiber.dispose() }) }) diff --git a/packages/web/web-search-deepseek/README.md b/packages/web/web-search-deepseek/README.md index 41b000d26a..6d65cbec5e 100644 --- a/packages/web/web-search-deepseek/README.md +++ b/packages/web/web-search-deepseek/README.md @@ -16,8 +16,8 @@ It reuses `$DEEPSEEK_API_KEY` (no new secret) but **not** `$DEEPSEEK_BASE_URL`: | Key | Default | Meaning | |---|---|---| -| `apiKey` | `$DEEPSEEK_API_KEY` | DeepSeek API key. Empty/absent → provider `status()` reports `missing-credential`. Sent as both `x-api-key` and `Authorization: Bearer` (official vs Anthropic-compatible proxy). | -| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic-compatible endpoint base; `/messages` is appended. Use a separate env var such as `$DEEPSEEK_SEARCH_BASE_URL` when overriding it; do not reuse `$DEEPSEEK_BASE_URL`, which belongs to the chat-completions LLM adapter. An unparseable value makes `status()` report `misconfigured`. | +| `apiKey` | `$DEEPSEEK_API_KEY` | DeepSeek API key. Empty/absent makes the provider unavailable. Sent as both `x-api-key` and `Authorization: Bearer` (official vs Anthropic-compatible proxy). | +| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic-compatible endpoint base; `/messages` is appended. Use a separate env var such as `$DEEPSEEK_SEARCH_BASE_URL` when overriding it; do not reuse `$DEEPSEEK_BASE_URL`, which belongs to the chat-completions LLM adapter. An unparseable value makes the provider unavailable. | | `model` | `deepseek-v4-flash` | Anthropic-format model name. | | `apiVersion` | `2023-06-01` | `anthropic-version` header value. | | `maxTokens` | `4096` | Positive-integer upper bound on generated tokens for the Messages request. | diff --git a/packages/web/web-search-deepseek/src/provider.ts b/packages/web/web-search-deepseek/src/provider.ts index fca8620e2a..d1bb82c00e 100644 --- a/packages/web/web-search-deepseek/src/provider.ts +++ b/packages/web/web-search-deepseek/src/provider.ts @@ -22,7 +22,6 @@ import { WebError } from '@deepseek-ai/dsh-web' import type { - WebProviderStatus, WebSearchProvider, WebSearchRequest, WebSearchResult, @@ -64,7 +63,7 @@ const USER_AGENT = 'deepseek-harness/0.0.1' /** Resolved provider options (the plugin's `apply` supplies env-var and constant defaults). */ export interface DeepSeekSearchProviderOptions { - /** DeepSeek API key. Empty/absent → `status()` reports `missing-credential`. */ + /** DeepSeek API key. Empty/absent makes the provider unavailable. */ apiKey: string /** Endpoint base; `/messages` is appended. */ baseURL: string @@ -111,11 +110,10 @@ export function citationSnippets(blocks: readonly ContentBlock[]): Map block.type === 'web_search_tool_result', @@ -143,7 +141,7 @@ export function mapAnthropicResponse(query: string, response: AnthropicResponse) }) } } - return { providerId: DEEPSEEK_PROVIDER_ID, query, sources, truncated: false } + return { sources, truncated: false } } /** The DeepSeek-backed search provider. */ @@ -152,14 +150,14 @@ export class DeepSeekSearchProvider implements WebSearchProvider { constructor(private readonly options: DeepSeekSearchProviderOptions) {} - status(): WebProviderStatus { - if (this.options.apiKey.length === 0) return { available: false, reason: 'missing-credential' } - if (!URL.canParse(this.options.baseURL)) return { available: false, reason: 'misconfigured' } - if (!isPositiveInteger(this.options.maxTokens) || !isPositiveInteger(this.options.maxUses)) return { available: false, reason: 'misconfigured' } - return { available: true } + available(): boolean { + return this.options.apiKey.length > 0 + && URL.canParse(this.options.baseURL) + && isPositiveInteger(this.options.maxTokens) + && isPositiveInteger(this.options.maxUses) } - async search(request: WebSearchRequest, exec?: { readonly signal?: AbortSignal }): Promise { + async search(request: WebSearchRequest, signal?: AbortSignal): Promise { let response: Response try { response = await fetch(`${this.options.baseURL}/messages`, { @@ -183,7 +181,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider { }], tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: this.options.maxUses }], }), - ...exec?.signal ? { signal: exec.signal } : {}, + ...signal !== undefined ? { signal } : {}, }) } catch (error: unknown) { if (isAbortError(error)) throw new WebError('DeepSeek search aborted', 'WEB_ABORTED', { cause: error }) @@ -211,7 +209,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider { try { const payload = await response.json() as AnthropicResponse - return mapAnthropicResponse(request.query, payload) + return mapAnthropicResponse(payload) } catch (error: unknown) { if (isAbortError(error)) throw new WebError('DeepSeek search aborted', 'WEB_ABORTED', { cause: error }) if (error instanceof WebError) throw error diff --git a/packages/web/web-search-deepseek/tests/deepseek.e2e.ts b/packages/web/web-search-deepseek/tests/deepseek.e2e.ts index f828fbd12e..03c99f9d9b 100644 --- a/packages/web/web-search-deepseek/tests/deepseek.e2e.ts +++ b/packages/web/web-search-deepseek/tests/deepseek.e2e.ts @@ -29,7 +29,6 @@ maybe('DeepSeekSearchProvider real API', () => { maxUses: DEEPSEEK_DEFAULT_MAX_USES, }) const result = await provider.search({ query: 'What is the DeepSeek Harness SDK?', maxResults: 5 }) - expect(result.providerId).toBe('deepseek') expect(result.sources.length).toBeGreaterThan(0) for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//) }, 60_000) diff --git a/packages/web/web-search-deepseek/tests/deepseek.spec.ts b/packages/web/web-search-deepseek/tests/deepseek.spec.ts index 0faab1f35a..12ee4ba55a 100644 --- a/packages/web/web-search-deepseek/tests/deepseek.spec.ts +++ b/packages/web/web-search-deepseek/tests/deepseek.spec.ts @@ -64,10 +64,8 @@ describe('citationSnippets', () => { describe('mapAnthropicResponse', () => { it('joins result items to citation snippets and maps page_age to publishedAt', () => { - const result = mapAnthropicResponse('q', searchResponse()) + const result = mapAnthropicResponse(searchResponse()) expect(result).toEqual({ - providerId: DEEPSEEK_PROVIDER_ID, - query: 'q', sources: [ { url: 'https://a.test', title: 'A', snippet: 'excerpt for A', publishedAt: '2026-02-02' }, { url: 'https://b.test', title: 'B' }, @@ -77,7 +75,7 @@ describe('mapAnthropicResponse', () => { }) it('dedupes repeated urls across result blocks (first wins)', () => { - const result = mapAnthropicResponse('q', { + const result = mapAnthropicResponse({ content: [ { type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: 'first' }] }, { type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: 'second' }] }, @@ -87,7 +85,7 @@ describe('mapAnthropicResponse', () => { }) it('skips non-result items and items with an empty url', () => { - const result = mapAnthropicResponse('q', { + const result = mapAnthropicResponse({ content: [{ type: 'web_search_tool_result', content: [ @@ -101,14 +99,14 @@ describe('mapAnthropicResponse', () => { }) it('omits optional fields when absent or empty', () => { - const result = mapAnthropicResponse('q', { + const result = mapAnthropicResponse({ content: [{ type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: '', page_age: '' }] }], }) expect(result.sources).toEqual([{ url: 'https://a.test' }]) }) it('tolerates a text block with no citations', () => { - const result = mapAnthropicResponse('q', { + const result = mapAnthropicResponse({ content: [ { type: 'text', text: 'no citations here' }, { type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: 'A' }] }, @@ -118,7 +116,7 @@ describe('mapAnthropicResponse', () => { }) it('tolerates a result block with no content array', () => { - const result = mapAnthropicResponse('q', { + const result = mapAnthropicResponse({ content: [ { type: 'web_search_tool_result' }, { type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test' }] }, @@ -128,38 +126,33 @@ describe('mapAnthropicResponse', () => { }) it('throws WEB_PROVIDER_ERROR (strict mode) when no result block is present', () => { - expect(() => mapAnthropicResponse('q', { content: [{ type: 'text', text: 'just prose, no search' }] })) + expect(() => mapAnthropicResponse({ content: [{ type: 'text', text: 'just prose, no search' }] })) .toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_ERROR' })) }) it('throws WEB_PROVIDER_ERROR when content is absent entirely', () => { - expect(() => mapAnthropicResponse('q', {})) + expect(() => mapAnthropicResponse({})) .toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_ERROR' })) }) }) -describe('DeepSeekSearchProvider status', () => { +describe('DeepSeekSearchProvider availability', () => { it('is unavailable without a key', () => { - expect(new DeepSeekSearchProvider({ ...options, apiKey: '' }).status()) - .toEqual({ available: false, reason: 'missing-credential' }) + expect(new DeepSeekSearchProvider({ ...options, apiKey: '' }).available()).toBe(false) }) it('is available with a key', () => { - expect(new DeepSeekSearchProvider(options).status()).toEqual({ available: true }) + expect(new DeepSeekSearchProvider(options).available()).toBe(true) }) it('is misconfigured when the base URL is unparseable', () => { - expect(new DeepSeekSearchProvider({ ...options, baseURL: 'not a url' }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) + expect(new DeepSeekSearchProvider({ ...options, baseURL: 'not a url' }).available()).toBe(false) }) it('is misconfigured when request limits are not positive integers', () => { - expect(new DeepSeekSearchProvider({ ...options, maxTokens: 0 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) - expect(new DeepSeekSearchProvider({ ...options, maxUses: 0 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) - expect(new DeepSeekSearchProvider({ ...options, maxUses: 1.5 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) + expect(new DeepSeekSearchProvider({ ...options, maxTokens: 0 }).available()).toBe(false) + expect(new DeepSeekSearchProvider({ ...options, maxUses: 0 }).available()).toBe(false) + expect(new DeepSeekSearchProvider({ ...options, maxUses: 1.5 }).available()).toBe(false) }) }) @@ -186,7 +179,7 @@ describe('DeepSeekSearchProvider request mapping', () => { const fetchMock = vi.fn(async () => jsonResponse(searchResponse())) vi.stubGlobal('fetch', fetchMock) const controller = new AbortController() - await new DeepSeekSearchProvider(options).search({ query: 'q' }, { signal: controller.signal }) + await new DeepSeekSearchProvider(options).search({ query: 'q' }, controller.signal) const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit] expect(init.signal).toBe(controller.signal) }) @@ -268,7 +261,7 @@ describe('web-search-deepseek plugin registration', () => { const ctx = new Context() await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID }) const fiber = await ctx.plugin(deepseekPlugin, { apiKey: 'ds-key' }) - await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: DEEPSEEK_PROVIDER_ID }) + await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ truncated: false }) await fiber.dispose() await expect(ctx.web.search({ query: 'q' })) .rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_MISSING' })) @@ -324,7 +317,7 @@ describe('web-search-deepseek plugin registration', () => { const unwrapped = loader.unwrapExports(deepseekPlugin) as Parameters[0] // A collapsed export shape (dropped inject) would throw "without inject" here. const fiber = await ctx.plugin(unwrapped, { apiKey: 'ds-key' }) - await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: DEEPSEEK_PROVIDER_ID }) + await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ truncated: false }) await fiber.dispose() }) diff --git a/packages/web/web-search-exa/README.md b/packages/web/web-search-exa/README.md index 0bc58d6559..6981281c40 100644 --- a/packages/web/web-search-exa/README.md +++ b/packages/web/web-search-exa/README.md @@ -8,8 +8,8 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i | Key | Default | Meaning | |---|---|---| -| `apiKey` | `$EXA_API_KEY` | Exa API key. Empty/absent → provider `status()` reports `missing-credential` (the seam reports `configured-unavailable`/`none`). | -| `baseURL` | `https://api.exa.ai` | Endpoint base; `/search` is appended. An unparseable value makes `status()` report `misconfigured`. | +| `apiKey` | `$EXA_API_KEY` | Exa API key. Empty/absent makes the provider unavailable. | +| `baseURL` | `https://api.exa.ai` | Endpoint base; `/search` is appended. An unparseable value makes the provider unavailable. | | `searchType` | `auto` | Retrieval mode sent as Exa's `type`: `auto` (Exa decides), `keyword`, or `neural`. | | `numResults` | (unset) | Default result count when a request carries no `maxResults`. Unset sends no default. Must be a positive integer. | | `highlightsPerResult` | `1` | Highlight sentences requested per result (Exa's `highlightsPerUrl`). Must be a positive integer. | diff --git a/packages/web/web-search-exa/src/provider.ts b/packages/web/web-search-exa/src/provider.ts index 48514c07bd..618c88fe3a 100644 --- a/packages/web/web-search-exa/src/provider.ts +++ b/packages/web/web-search-exa/src/provider.ts @@ -14,7 +14,6 @@ import { WebError } from '@deepseek-ai/dsh-web' import type { - WebProviderStatus, WebSearchProvider, WebSearchRequest, WebSearchResult, @@ -39,7 +38,7 @@ const USER_AGENT = 'deepseek-harness/0.0.1' /** Resolved provider options (the plugin's `apply` supplies env-var and constant defaults). */ export interface ExaSearchProviderOptions { - /** Exa API key. Empty/absent → `status()` reports `missing-credential`. */ + /** Exa API key. Empty/absent makes the provider unavailable. */ apiKey: string /** Endpoint base; `/search` is appended. */ baseURL: string @@ -74,18 +73,17 @@ export function mapExaResult(result: ExaResult): WebSearchSource | undefined { /** * Map an Exa response envelope to a normalized search result. * - * @param query - the original request query, echoed on the result. * @param response - the parsed `POST /search` response body. * @returns the normalized result; snippet-less entries are dropped * ({@link mapExaResult}). */ -export function mapExaResponse(query: string, response: ExaSearchResponse): WebSearchResult { +export function mapExaResponse(response: ExaSearchResponse): WebSearchResult { const sources = (response.results ?? []) .map(mapExaResult) .filter((source): source is WebSearchSource => source !== undefined) // Exa returns no generated answer, so `content` is omitted. The seam owns the // final `maxResults` truncation, so this provider reports `truncated: false`. - return { providerId: EXA_PROVIDER_ID, query, sources, truncated: false } + return { sources, truncated: false } } /** The Exa-backed search provider. */ @@ -94,15 +92,14 @@ export class ExaSearchProvider implements WebSearchProvider { constructor(private readonly options: ExaSearchProviderOptions) {} - status(): WebProviderStatus { - if (this.options.apiKey.length === 0) return { available: false, reason: 'missing-credential' } - if (!isValidBaseUrl(this.options.baseURL)) return { available: false, reason: 'misconfigured' } - if (!isPositiveInteger(this.options.highlightsPerResult)) return { available: false, reason: 'misconfigured' } - if (this.options.numResults !== undefined && !isPositiveInteger(this.options.numResults)) return { available: false, reason: 'misconfigured' } - return { available: true } + available(): boolean { + return this.options.apiKey.length > 0 + && isValidBaseUrl(this.options.baseURL) + && isPositiveInteger(this.options.highlightsPerResult) + && (this.options.numResults === undefined || isPositiveInteger(this.options.numResults)) } - async search(request: WebSearchRequest, exec?: { readonly signal?: AbortSignal }): Promise { + async search(request: WebSearchRequest, signal?: AbortSignal): Promise { // A per-request bound wins over the configured default; either may be absent. const numResults = request.maxResults ?? this.options.numResults let response: Response @@ -121,7 +118,7 @@ export class ExaSearchProvider implements WebSearchProvider { contents: { highlights: { highlightsPerUrl: this.options.highlightsPerResult } }, ...numResults !== undefined ? { numResults } : {}, }), - ...exec?.signal ? { signal: exec.signal } : {}, + ...signal !== undefined ? { signal } : {}, }) } catch (error: unknown) { if (isAbortError(error)) throw new WebError('Exa search aborted', 'WEB_ABORTED', { cause: error }) @@ -149,7 +146,7 @@ export class ExaSearchProvider implements WebSearchProvider { try { const payload = await response.json() as ExaSearchResponse - return mapExaResponse(request.query, payload) + return mapExaResponse(payload) } catch (error: unknown) { if (isAbortError(error)) throw new WebError('Exa search aborted', 'WEB_ABORTED', { cause: error }) throw new WebError(`Exa returned an unprocessable response body: ${String(error)}`, 'WEB_PROVIDER_ERROR', { cause: error }) diff --git a/packages/web/web-search-exa/tests/exa.e2e.ts b/packages/web/web-search-exa/tests/exa.e2e.ts index c9e9233bc0..ff04371881 100644 --- a/packages/web/web-search-exa/tests/exa.e2e.ts +++ b/packages/web/web-search-exa/tests/exa.e2e.ts @@ -17,7 +17,6 @@ maybe('ExaSearchProvider real API', () => { highlightsPerResult: EXA_DEFAULT_HIGHLIGHTS_PER_RESULT, }) const result = await provider.search({ query: 'DeepSeek Harness SDK', maxResults: 5 }) - expect(result.providerId).toBe('exa') expect(result.sources.length).toBeGreaterThan(0) for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//) }, 30_000) diff --git a/packages/web/web-search-exa/tests/exa.spec.ts b/packages/web/web-search-exa/tests/exa.spec.ts index 204b18f702..cf66ea67f9 100644 --- a/packages/web/web-search-exa/tests/exa.spec.ts +++ b/packages/web/web-search-exa/tests/exa.spec.ts @@ -38,7 +38,7 @@ describe('Exa result mapping', () => { }) it('maps a response to a result with no content and filtered sources', () => { - const result = mapExaResponse('q', { + const result = mapExaResponse({ results: [ { url: 'https://a.test', highlights: ['one'] }, { url: 'https://b.test' }, @@ -46,8 +46,6 @@ describe('Exa result mapping', () => { ], }) expect(result).toEqual({ - providerId: EXA_PROVIDER_ID, - query: 'q', sources: [ { url: 'https://a.test', snippet: 'one' }, { url: 'https://c.test', title: 'C', snippet: 'three' }, @@ -58,36 +56,31 @@ describe('Exa result mapping', () => { }) it('tolerates a missing results array', () => { - expect(mapExaResponse('q', {}).sources).toEqual([]) + expect(mapExaResponse({}).sources).toEqual([]) }) }) -describe('ExaSearchProvider status', () => { +describe('ExaSearchProvider availability', () => { it('is unavailable without a key', () => { - expect(new ExaSearchProvider({ ...options, apiKey: '' }).status()) - .toEqual({ available: false, reason: 'missing-credential' }) + expect(new ExaSearchProvider({ ...options, apiKey: '' }).available()).toBe(false) }) it('is available with a key', () => { - expect(new ExaSearchProvider(options).status()).toEqual({ available: true }) + expect(new ExaSearchProvider(options).available()).toBe(true) }) it('is misconfigured when the base URL is unparseable', () => { - expect(new ExaSearchProvider({ ...options, baseURL: 'not a url' }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) + expect(new ExaSearchProvider({ ...options, baseURL: 'not a url' }).available()).toBe(false) }) it('is misconfigured when highlightsPerResult is not a positive integer', () => { - expect(new ExaSearchProvider({ ...options, highlightsPerResult: 0 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) - expect(new ExaSearchProvider({ ...options, highlightsPerResult: 1.5 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) + expect(new ExaSearchProvider({ ...options, highlightsPerResult: 0 }).available()).toBe(false) + expect(new ExaSearchProvider({ ...options, highlightsPerResult: 1.5 }).available()).toBe(false) }) it('is misconfigured when numResults is set but not a positive integer', () => { - expect(new ExaSearchProvider({ ...options, numResults: -1 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) + expect(new ExaSearchProvider({ ...options, numResults: -1 }).available()).toBe(false) }) }) @@ -139,7 +132,7 @@ describe('ExaSearchProvider request mapping', () => { const fetchMock = vi.fn(async () => jsonResponse({ results: [] })) vi.stubGlobal('fetch', fetchMock) const controller = new AbortController() - await new ExaSearchProvider(options).search({ query: 'q' }, { signal: controller.signal }) + await new ExaSearchProvider(options).search({ query: 'q' }, controller.signal) const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit] expect(init.signal).toBe(controller.signal) }) @@ -209,7 +202,7 @@ describe('web-search-exa plugin registration', () => { const ctx = new Context() await ctx.plugin(WebService, { searchProvider: EXA_PROVIDER_ID }) const fiber = await ctx.plugin(exaPlugin, { apiKey: 'exa-key' }) - await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: EXA_PROVIDER_ID }) + await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ sources: [], truncated: false }) await fiber.dispose() await expect(ctx.web.search({ query: 'q' })) .rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_MISSING' })) diff --git a/packages/web/web-search-perplexity/README.md b/packages/web/web-search-perplexity/README.md index f944413c96..1a17026771 100644 --- a/packages/web/web-search-perplexity/README.md +++ b/packages/web/web-search-perplexity/README.md @@ -8,8 +8,8 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i | Key | Default | Meaning | |---|---|---| -| `apiKey` | `$PERPLEXITY_API_KEY` | Perplexity API key. Empty/absent → provider `status()` reports `missing-credential`. | -| `baseURL` | `https://api.perplexity.ai` | Endpoint base; `/chat/completions` is appended. An unparseable value makes `status()` report `misconfigured`. | +| `apiKey` | `$PERPLEXITY_API_KEY` | Perplexity API key. Empty/absent makes the provider unavailable. | +| `baseURL` | `https://api.perplexity.ai` | Endpoint base; `/chat/completions` is appended. An unparseable value makes the provider unavailable. | | `model` | `sonar` | Search model name. | | `maxTokens` | `1024` | Upper bound on generated answer tokens (`max_tokens`). Must be a positive integer. | | `searchRecency` | (unset) | Recency window sent as `search_recency_filter`: `day`, `week`, `month`, or `year`. Unset sends no filter. | diff --git a/packages/web/web-search-perplexity/src/provider.ts b/packages/web/web-search-perplexity/src/provider.ts index 3f1959549a..2efa735516 100644 --- a/packages/web/web-search-perplexity/src/provider.ts +++ b/packages/web/web-search-perplexity/src/provider.ts @@ -15,7 +15,6 @@ import { WebError } from '@deepseek-ai/dsh-web' import type { - WebProviderStatus, WebSearchProvider, WebSearchRequest, WebSearchResult, @@ -43,7 +42,7 @@ const USER_AGENT = 'deepseek-harness/0.0.1' /** Resolved provider options (the plugin's `apply` supplies env-var and constant defaults). */ export interface PerplexitySearchProviderOptions { - /** Perplexity API key. Empty/absent → `status()` reports `missing-credential`. */ + /** Perplexity API key. Empty/absent makes the provider unavailable. */ apiKey: string /** Endpoint base; `/chat/completions` is appended. */ baseURL: string @@ -75,18 +74,15 @@ export function mapPerplexityResult(result: PerplexitySearchResult): WebSearchSo * structured `search_results[]`; falls back to URL-only `citations[]` (those * sources carry just a `url`) only when `search_results` is absent. * - * @param query - the original request query, echoed on the result. * @param response - the parsed chat-completions response body. * @returns the normalized result; `content` is omitted when the answer is empty. */ -export function mapPerplexityResponse(query: string, response: PerplexityResponse): WebSearchResult { +export function mapPerplexityResponse(response: PerplexityResponse): WebSearchResult { const content = response.choices?.[0]?.message?.content const sources: WebSearchSource[] = response.search_results !== undefined ? response.search_results.map(mapPerplexityResult) : (response.citations ?? []).map(url => ({ url })) return { - providerId: PERPLEXITY_PROVIDER_ID, - query, ...content != null && content.length > 0 ? { content } : {}, sources, truncated: false, @@ -102,15 +98,14 @@ export class PerplexitySearchProvider implements WebSearchProvider { // Availability checks stay beside each provider's distinct config contract; // a shared base class would obscure which fields make this backend usable. /* jscpd:ignore-start */ - status(): WebProviderStatus { - if (this.options.apiKey.length === 0) return { available: false, reason: 'missing-credential' } - if (!URL.canParse(this.options.baseURL)) return { available: false, reason: 'misconfigured' } - if (!isPositiveInteger(this.options.maxTokens)) return { available: false, reason: 'misconfigured' } - return { available: true } + available(): boolean { + return this.options.apiKey.length > 0 + && URL.canParse(this.options.baseURL) + && isPositiveInteger(this.options.maxTokens) } /* jscpd:ignore-end */ - async search(request: WebSearchRequest, exec?: { readonly signal?: AbortSignal }): Promise { + async search(request: WebSearchRequest, signal?: AbortSignal): Promise { let response: Response try { response = await fetch(`${this.options.baseURL}/chat/completions`, { @@ -127,7 +122,7 @@ export class PerplexitySearchProvider implements WebSearchProvider { messages: [{ role: 'user', content: request.query }], ...this.options.searchRecency !== undefined ? { search_recency_filter: this.options.searchRecency } : {}, }), - ...exec?.signal ? { signal: exec.signal } : {}, + ...signal !== undefined ? { signal } : {}, }) } catch (error: unknown) { if (isAbortError(error)) throw new WebError('Perplexity search aborted', 'WEB_ABORTED', { cause: error }) @@ -155,7 +150,7 @@ export class PerplexitySearchProvider implements WebSearchProvider { try { const payload = await response.json() as PerplexityResponse - return mapPerplexityResponse(request.query, payload) + return mapPerplexityResponse(payload) } catch (error: unknown) { if (isAbortError(error)) throw new WebError('Perplexity search aborted', 'WEB_ABORTED', { cause: error }) throw new WebError(`Perplexity returned an unprocessable response body: ${String(error)}`, 'WEB_PROVIDER_ERROR', { cause: error }) diff --git a/packages/web/web-search-perplexity/tests/perplexity.e2e.ts b/packages/web/web-search-perplexity/tests/perplexity.e2e.ts index 2fc89db7d4..e8f7474caa 100644 --- a/packages/web/web-search-perplexity/tests/perplexity.e2e.ts +++ b/packages/web/web-search-perplexity/tests/perplexity.e2e.ts @@ -17,7 +17,6 @@ maybe('PerplexitySearchProvider real API', () => { maxTokens: PERPLEXITY_DEFAULT_MAX_TOKENS, }) const result = await provider.search({ query: 'What is the DeepSeek Harness SDK?', maxResults: 5 }) - expect(result.providerId).toBe('perplexity') expect(result.content ?? '').not.toBe('') for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//) }, 30_000) diff --git a/packages/web/web-search-perplexity/tests/perplexity.spec.ts b/packages/web/web-search-perplexity/tests/perplexity.spec.ts index df8a98f003..e769989d73 100644 --- a/packages/web/web-search-perplexity/tests/perplexity.spec.ts +++ b/packages/web/web-search-perplexity/tests/perplexity.spec.ts @@ -20,7 +20,7 @@ afterEach(() => { describe('Perplexity response mapping', () => { it('maps the answer and prefers structured search_results', () => { - const result = mapPerplexityResponse('q', { + const result = mapPerplexityResponse({ choices: [{ message: { content: 'the answer' } }], search_results: [ { url: 'https://a.test', title: 'A', snippet: 'snip', date: '2026-02-02' }, @@ -29,8 +29,6 @@ describe('Perplexity response mapping', () => { citations: ['https://ignored.test'], }) expect(result).toEqual({ - providerId: PERPLEXITY_PROVIDER_ID, - query: 'q', content: 'the answer', sources: [ { url: 'https://a.test', title: 'A', snippet: 'snip', publishedAt: '2026-02-02' }, @@ -41,7 +39,7 @@ describe('Perplexity response mapping', () => { }) it('falls back to URL-only citations when search_results is absent', () => { - const result = mapPerplexityResponse('q', { + const result = mapPerplexityResponse({ choices: [{ message: { content: 'answer' } }], citations: ['https://a.test', 'https://b.test'], }) @@ -49,43 +47,39 @@ describe('Perplexity response mapping', () => { }) it('omits content when the answer is empty or missing', () => { - expect(mapPerplexityResponse('q', { citations: [] }).content).toBeUndefined() - expect(mapPerplexityResponse('q', { choices: [{ message: { content: '' } }] }).content).toBeUndefined() - expect(mapPerplexityResponse('q', { choices: [{ message: { content: null } }] }).content).toBeUndefined() + expect(mapPerplexityResponse({ citations: [] }).content).toBeUndefined() + expect(mapPerplexityResponse({ choices: [{ message: { content: '' } }] }).content).toBeUndefined() + expect(mapPerplexityResponse({ choices: [{ message: { content: null } }] }).content).toBeUndefined() }) it('omits null/empty optional source fields', () => { - const result = mapPerplexityResponse('q', { + const result = mapPerplexityResponse({ search_results: [{ url: 'https://a.test', title: null, snippet: '', date: null }], }) expect(result.sources).toEqual([{ url: 'https://a.test' }]) }) it('yields no sources when neither search_results nor citations are present', () => { - expect(mapPerplexityResponse('q', { choices: [{ message: { content: 'a' } }] }).sources).toEqual([]) + expect(mapPerplexityResponse({ choices: [{ message: { content: 'a' } }] }).sources).toEqual([]) }) }) -describe('PerplexitySearchProvider status', () => { +describe('PerplexitySearchProvider availability', () => { it('is unavailable without a key', () => { - expect(new PerplexitySearchProvider({ ...options, apiKey: '' }).status()) - .toEqual({ available: false, reason: 'missing-credential' }) + expect(new PerplexitySearchProvider({ ...options, apiKey: '' }).available()).toBe(false) }) it('is available with a key', () => { - expect(new PerplexitySearchProvider(options).status()).toEqual({ available: true }) + expect(new PerplexitySearchProvider(options).available()).toBe(true) }) it('is misconfigured when the base URL is unparseable', () => { - expect(new PerplexitySearchProvider({ ...options, baseURL: 'not a url' }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) + expect(new PerplexitySearchProvider({ ...options, baseURL: 'not a url' }).available()).toBe(false) }) it('is misconfigured when maxTokens is not a positive integer', () => { - expect(new PerplexitySearchProvider({ ...options, maxTokens: 0 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) - expect(new PerplexitySearchProvider({ ...options, maxTokens: 1.5 }).status()) - .toEqual({ available: false, reason: 'misconfigured' }) + expect(new PerplexitySearchProvider({ ...options, maxTokens: 0 }).available()).toBe(false) + expect(new PerplexitySearchProvider({ ...options, maxTokens: 1.5 }).available()).toBe(false) }) }) @@ -114,7 +108,7 @@ describe('PerplexitySearchProvider request mapping', () => { const fetchMock = vi.fn(async () => jsonResponse({ citations: [] })) vi.stubGlobal('fetch', fetchMock) const controller = new AbortController() - await new PerplexitySearchProvider(options).search({ query: 'q' }, { signal: controller.signal }) + await new PerplexitySearchProvider(options).search({ query: 'q' }, controller.signal) const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit] expect(init.signal).toBe(controller.signal) }) @@ -190,7 +184,7 @@ describe('web-search-perplexity plugin registration', () => { const ctx = new Context() await ctx.plugin(WebService, { searchProvider: PERPLEXITY_PROVIDER_ID }) const fiber = await ctx.plugin(perplexityPlugin, { apiKey: 'pplx-key' }) - await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: PERPLEXITY_PROVIDER_ID }) + await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ content: 'a', sources: [] }) await fiber.dispose() await expect(ctx.web.search({ query: 'q' })) .rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_MISSING' })) diff --git a/packages/web/web/README.md b/packages/web/web/README.md index fe5f1e650e..ce664ee6d8 100644 --- a/packages/web/web/README.md +++ b/packages/web/web/README.md @@ -19,8 +19,8 @@ Search and fetch share no request schema and no business logic, but they are del | Member | Semantics | |---|---| | `registerSearchProvider(provider)` / `registerFetchProvider(provider)` | Register a backend. Throws `WebError` `WEB_DUPLICATE_PROVIDER` on a duplicate id within that capability kind. Returns a disposer. Disposed with the calling fiber. | -| `search(request, exec?)` | Resolve the search provider and run one search. Enforces `request.maxResults` on the result (truncates `sources[]`, sets `truncated`). Throws `WebError` when the capability cannot run. | -| `fetch(request, exec?)` | Resolve the fetch provider and retrieve one URL. A non-2xx response is a result, not a throw. Throws `WebError` for failures to safely retrieve or represent the resource. | +| `search(request, signal?)` | Resolve the search provider and run one search. Enforces `request.maxResults` on the result (truncates `sources[]`, sets `truncated`). Throws `WebError` when the capability cannot run. | +| `fetch(request, signal?)` | Resolve the fetch provider and retrieve one URL. A non-2xx response is a result, not a throw. Throws `WebError` for failures to safely retrieve or represent the resource. | Providers register **capabilities**, not tools. `dsh-tool-web` is the only owner of model-facing names, descriptions, prompt guidance, JSON schemas, and presentation. @@ -30,15 +30,15 @@ Selection never depends on registration, config, or HMR order. A capability has | Situation | Execution | |---|---| -| configured id registered and `status().available` | runs that provider | +| configured id registered and `available()` | runs that provider | | configured id not registered | `WEB_PROVIDER_CONFIGURED_MISSING` | | configured id registered but unavailable | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` | | no id, exactly one registered usable provider | runs it | | no id, no usable provider | `WEB_PROVIDER_UNAVAILABLE` | | no id, multiple usable providers | `WEB_PROVIDER_AMBIGUOUS` | -The failure branches throw `WebError`, whose structured code (plus message detail — the missing id, the ambiguous candidate set) is the surface callers route on. A provider's own `status()` is a cheap local check (credential presence, parseable config) that feeds this execution-time selection and **must not make network calls**; `dsh-tool-web` never calls a provider's `status()` — it executes through `ctx.web.search()`/`fetch()` and routes on the thrown codes, so provider selection has one owner. +The failure branches throw `WebError`, whose structured code (plus message detail — the missing id, the ambiguous candidate set) is the surface callers route on. A provider's own `available()` is a cheap local check (credential presence, parseable config) that feeds this execution-time selection and **must not make network calls**; `dsh-tool-web` never calls it — the tool executes through `ctx.web.search()`/`fetch()` and routes on the thrown codes, so provider selection has one owner. ## Vocabulary -`WebSearchRequest` (`query`, `maxResults?`) → `WebSearchResult` (`providerId`, `query`, `content?`, `sources[]`, `truncated`); each `WebSearchSource` has a required `url` and optional `title`/`snippet`/`publishedAt` (Perplexity citations may be URL-only). `WebFetchRequest` (`url`, `timeoutMs?`) → `WebFetchResult` (`providerId`, final `url`, `statusCode`, `body`, `truncated`); `WebFetchBody` is a CLOSED discriminated union (`html` | `text`) owned here — consumers `switch` to exhaustiveness so a new kind breaks their compilation until handled. See `src/types.ts` for the full contracts and the `WebError` code taxonomy. +`WebSearchRequest` (`query`, `maxResults?`) → `WebSearchResult` (`content?`, `sources[]`, `truncated`); each `WebSearchSource` has a required `url` and optional `title`/`snippet`/`publishedAt` (Perplexity citations may be URL-only). `WebFetchRequest` (`url`) → `WebFetchResult` (final `url`, `statusCode`, `body`, `truncated`); cancellation is a direct optional `AbortSignal` argument to `search()`/`fetch()`. `WebFetchBody` is a CLOSED discriminated union (`html` | `text`) owned here — consumers `switch` to exhaustiveness so a new kind breaks their compilation until handled. See `src/types.ts` for the full contracts and the `WebError` code taxonomy. diff --git a/packages/web/web/src/index.ts b/packages/web/web/src/index.ts index 5da736a85a..39620777b5 100644 --- a/packages/web/web/src/index.ts +++ b/packages/web/web/src/index.ts @@ -18,11 +18,9 @@ import { Context, Service } from 'cordis' import z from 'schemastery' import type { - WebExecContext, WebFetchProvider, WebFetchRequest, WebFetchResult, - WebProviderStatus, WebSearchProvider, WebSearchRequest, WebSearchResult, @@ -33,12 +31,10 @@ export { WebError, } from './types.ts' export type { - WebExecContext, WebFetchBody, WebFetchProvider, WebFetchRequest, WebFetchResult, - WebProviderStatus, WebSearchProvider, WebSearchRequest, WebSearchResult, @@ -76,7 +72,7 @@ export interface WebServiceConfig { * The web access service. Registered as `ctx.web` (one instance per context). * * Selection semantics (resolved at execution time, never order-dependent): - * - A configured id that is registered and `status().available` → that provider. + * - A configured id that is registered and `available()` → that provider. * - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`. * - A configured id registered but unavailable → * `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. @@ -147,15 +143,15 @@ export class WebService extends Service { * capability cannot run. The seam enforces `request.maxResults` on the result: * if the provider over-returns, `sources[]` is truncated and `truncated` set. * @param request - the query plus result-shaping options. - * @param exec - the tool-execution context, forwarded to the provider. + * @param signal - optional cancellation signal forwarded to the provider. * @returns the provider's results, capped to `request.maxResults`. */ - async search(request: WebSearchRequest, exec?: WebExecContext): Promise { + async search(request: WebSearchRequest, signal?: AbortSignal): Promise { const provider = resolveProvider({ providers: this.searchProviders, ...this.searchProviderId !== undefined ? { configuredId: this.searchProviderId } : {}, }) - const result = await provider.search(request, exec) + const result = await provider.search(request, signal) return capSources(result, request.maxResults) } @@ -164,21 +160,21 @@ export class WebService extends Service { * call time with the selection rules above; throws {@link WebError} when the * capability cannot run. A non-2xx response is a result, not a throw. * @param request - the URL plus retrieval options. - * @param exec - the tool-execution context, forwarded to the provider. + * @param signal - optional cancellation signal forwarded to the provider. * @returns the retrieval outcome; non-2xx responses resolve descriptively. */ - async fetch(request: WebFetchRequest, exec?: WebExecContext): Promise { + async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise { const provider = resolveProvider({ providers: this.fetchProviders, ...this.fetchProviderId !== undefined ? { configuredId: this.fetchProviderId } : {}, }) - return provider.fetch(request, exec) + return provider.fetch(request, signal) } } interface ResolvableProvider { readonly id: string - status(): WebProviderStatus + available(): boolean } /** Resolve the selected provider or throw the matching {@link WebError}. */ @@ -189,12 +185,12 @@ function resolveProvider

(selection: Selection

): if (!provider) { throw new WebError(`configured web provider "${configuredId}" is not registered`, 'WEB_PROVIDER_CONFIGURED_MISSING') } - if (!provider.status().available) { + if (!provider.available()) { throw new WebError(`configured web provider "${configuredId}" is registered but unavailable`, 'WEB_PROVIDER_CONFIGURED_UNAVAILABLE') } return provider } - const usable = [...providers.values()].filter(provider => provider.status().available) + const usable = [...providers.values()].filter(provider => provider.available()) const [single] = usable if (single === undefined) { throw new WebError('no usable web provider is registered', 'WEB_PROVIDER_UNAVAILABLE') diff --git a/packages/web/web/src/types.ts b/packages/web/web/src/types.ts index f4cda691b8..d16855aa69 100644 --- a/packages/web/web/src/types.ts +++ b/packages/web/web/src/types.ts @@ -1,8 +1,7 @@ /** * Vocabulary for the web capability seam (`ctx.web`): the search/fetch - * request/result shapes providers produce and consumers format, the provider - * status discriminant selection reads, the execution-control context, and the - * typed error taxonomy. + * request/result shapes providers produce and consumers format, provider + * availability, direct cancellation control, and the typed error taxonomy. * * These types are shared by every provider backend * (`@deepseek-ai/dsh-web-search-exa`, `@deepseek-ai/dsh-web-search-perplexity`, @@ -19,19 +18,6 @@ import { HarnessError } from '@deepseek-ai/dsh-llm' -/** - * Execution control threaded from the tool layer through the seam into a - * provider's network requests, stream readers, and expensive decoding. It is - * NOT business input: the first version carries only `signal` so `tool-web` can - * propagate turn cancellation, tool timeout, and agent disposal. It deliberately - * does NOT carry `ToolExecution`, which would make `dsh-web` depend on - * `dsh-tools`. - */ -export interface WebExecContext { - /** Abort signal a provider must honor for its network/decoding work. */ - readonly signal?: AbortSignal -} - /** * What one search-capable backend can return. The model-facing argument is just * a query; `maxResults` is a `dsh-tool-web`-layer bound passed through unchanged @@ -56,10 +42,6 @@ export interface WebSearchRequest { * when it cut `sources[]` down to `maxResults`. */ export interface WebSearchResult { - /** Id of the provider that produced this result. */ - readonly providerId: string - /** Echo of the query the provider answered. */ - readonly query: string /** Optional provider-generated answer text, search context, or summary. */ readonly content?: string /** Citeable sources, already truncated to the request's `maxResults`. */ @@ -83,14 +65,13 @@ export interface WebSearchSource { } /** - * What one fetch-capable backend is asked to retrieve. `timeoutMs` is an - * optional positive hint the provider caps. The request deliberately omits - * `format`, `prompt`, and extraction controls — those are presentation or - * higher-level LLM concerns, not safe-retrieval inputs. + * What one fetch-capable backend is asked to retrieve. The request deliberately + * omits timeout, format, prompt, and extraction controls: cancellation is a + * direct execution argument, while presentation and higher-level LLM concerns + * belong outside safe retrieval. */ export interface WebFetchRequest { readonly url: string - readonly timeoutMs?: number } /** @@ -100,8 +81,6 @@ export interface WebFetchRequest { * represent the resource. */ export interface WebFetchResult { - /** Id of the provider that produced this result. */ - readonly providerId: string /** The final URL after allowed redirects (the request URL is in the request). */ readonly url: string /** HTTP status code of the fetched response. */ @@ -125,18 +104,6 @@ export type WebFetchBody = | { readonly kind: 'html'; readonly content: string } | { readonly kind: 'text'; readonly content: string } -/** - * Whether one concrete provider implementation is usable, by cheap local checks - * only (credential presence, parseable endpoint config). A provider `status()` - * must NOT make network calls. It is an input to execution-time selection, not - * a health system: `WebService.search()`/`fetch()` read it to pick a usable - * provider, and selection failure surfaces as the structured {@link WebError} - * codes callers route on. - */ -export type WebProviderStatus = - | { readonly available: true } - | { readonly available: false; readonly reason: 'missing-credential' | 'misconfigured' } - /** * A search-capable backend. Registered with `ctx.web.registerSearchProvider`. * `id` is a stable string, unique within the search capability kind. @@ -144,9 +111,9 @@ export type WebProviderStatus = export interface WebSearchProvider { readonly id: string /** Cheap local usability check; must not make network calls. */ - status(): WebProviderStatus - /** Run one search; honor `exec.signal` for cancellation. */ - search(request: WebSearchRequest, exec?: WebExecContext): Promise + available(): boolean + /** Run one search; honor `signal` for cancellation. */ + search(request: WebSearchRequest, signal?: AbortSignal): Promise } /** @@ -156,9 +123,9 @@ export interface WebSearchProvider { export interface WebFetchProvider { readonly id: string /** Cheap local usability check; must not make network calls. */ - status(): WebProviderStatus - /** Retrieve one URL; honor `exec.signal` for cancellation. */ - fetch(request: WebFetchRequest, exec?: WebExecContext): Promise + available(): boolean + /** Retrieve one URL; honor `signal` for cancellation. */ + fetch(request: WebFetchRequest, signal?: AbortSignal): Promise } /** @@ -178,12 +145,12 @@ export interface WebFetchProvider { * - `WEB_PROVIDER_UNAVAILABLE`: no provider configured and none usable. * - `WEB_PROVIDER_CONFIGURED_MISSING`: a configured id is not registered. * - `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`: a configured id is registered but its - * `status()` reports unavailable. + * `available()` returns false. * - `WEB_PROVIDER_AMBIGUOUS`: no id configured and multiple usable providers * exist (selection refuses to pick by registration order). * - `WEB_DUPLICATE_PROVIDER`: a registration-time programming error — an id is * already registered for that capability kind. - * - `WEB_ABORTED`: the operation was aborted via `WebExecContext.signal`. + * - `WEB_ABORTED`: the operation was aborted via its optional signal. * - `WEB_PROVIDER_ERROR`: catch-all for a provider's own failure surfaced * through the seam, including network/transport failure (DNS, connection * refused, TLS). diff --git a/packages/web/web/tests/web.spec.ts b/packages/web/web/tests/web.spec.ts index 8189e342da..978284ee51 100644 --- a/packages/web/web/tests/web.spec.ts +++ b/packages/web/web/tests/web.spec.ts @@ -4,7 +4,6 @@ import WebService, { WebError, type WebFetchProvider, type WebFetchResult, - type WebProviderStatus, type WebSearchProvider, type WebSearchRequest, type WebSearchResult, @@ -13,25 +12,25 @@ import WebService, { /** A scripted search provider for contract tests. */ function makeSearchProvider( id: string, - status: WebProviderStatus, + available: boolean, search: (request: WebSearchRequest) => Promise, ): WebSearchProvider { - return { id, status: () => status, search: request => search(request) } + return { id, available: () => available, search: request => search(request) } } -function makeFetchProvider(id: string, status: WebProviderStatus, result: WebFetchResult): WebFetchProvider { - return { id, status: () => status, fetch: () => Promise.resolve(result) } +function makeFetchProvider(id: string, available: boolean, result: WebFetchResult): WebFetchProvider { + return { id, available: () => available, fetch: () => Promise.resolve(result) } } -const available: WebProviderStatus = { available: true } -const unavailable: WebProviderStatus = { available: false, reason: 'missing-credential' } +const available = true +const unavailable = false -function searchResult(providerId: string, overrides: Partial = {}): WebSearchResult { - return { providerId, query: 'q', sources: [], truncated: false, ...overrides } +function searchResult(marker: string, overrides: Partial = {}): WebSearchResult { + return { content: marker, sources: [], truncated: false, ...overrides } } -function fetchResult(providerId: string): WebFetchResult { - return { providerId, url: 'https://example.com', statusCode: 200, body: { kind: 'text', content: 'hi' }, truncated: false } +function fetchResult(marker: string): WebFetchResult { + return { url: 'https://example.com', statusCode: 200, body: { kind: 'text', content: marker }, truncated: false } } /** Mount a WebService on a fresh root context with the given config. */ @@ -46,7 +45,7 @@ describe('WebService registration', () => { const { web } = await mountWeb() const dispose = web.registerSearchProvider(makeSearchProvider('exa', available, () => Promise.resolve(searchResult('exa')))) - await expect(web.search({ query: 'q' })).resolves.toMatchObject({ providerId: 'exa' }) + await expect(web.search({ query: 'q' })).resolves.toMatchObject({ content: 'exa' }) dispose() await expect(web.search({ query: 'q' })).rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_UNAVAILABLE' })) @@ -70,7 +69,7 @@ describe('WebService registration', () => { const fiber = await ctx.plugin(Object.assign((inner: Context) => { inner.web.registerSearchProvider(makeSearchProvider('exa', available, () => Promise.resolve(searchResult('exa')))) }, { inject: ['web'] })) - await expect(web.search({ query: 'q' })).resolves.toMatchObject({ providerId: 'exa' }) + await expect(web.search({ query: 'q' })).resolves.toMatchObject({ content: 'exa' }) await fiber.dispose() await expect(web.search({ query: 'q' })).rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_UNAVAILABLE' })) }) @@ -111,26 +110,26 @@ describe('WebService execution resolution', () => { const { web } = await mountWeb({ searchProvider: 'perplexity' }) web.registerSearchProvider(makeSearchProvider('exa', available, () => Promise.resolve(searchResult('exa')))) web.registerSearchProvider(makeSearchProvider('perplexity', available, () => Promise.resolve(searchResult('perplexity')))) - await expect(web.search({ query: 'q' })).resolves.toMatchObject({ providerId: 'perplexity' }) + await expect(web.search({ query: 'q' })).resolves.toMatchObject({ content: 'perplexity' }) }) it('ignores unusable providers when auto-selecting', async () => { const { web } = await mountWeb() web.registerSearchProvider(makeSearchProvider('exa', available, () => Promise.resolve(searchResult('exa')))) web.registerSearchProvider(makeSearchProvider('perplexity', unavailable, () => Promise.resolve(searchResult('perplexity')))) - await expect(web.search({ query: 'q' })).resolves.toMatchObject({ providerId: 'exa' }) + await expect(web.search({ query: 'q' })).resolves.toMatchObject({ content: 'exa' }) }) it('does not let registration order change auto-selection', async () => { const a = await mountWeb() a.web.registerSearchProvider(makeSearchProvider('exa', unavailable, () => Promise.resolve(searchResult('exa')))) a.web.registerSearchProvider(makeSearchProvider('perplexity', available, () => Promise.resolve(searchResult('perplexity')))) - await expect(a.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: 'perplexity' }) + await expect(a.web.search({ query: 'q' })).resolves.toMatchObject({ content: 'perplexity' }) const b = await mountWeb() b.web.registerSearchProvider(makeSearchProvider('perplexity', available, () => Promise.resolve(searchResult('perplexity')))) b.web.registerSearchProvider(makeSearchProvider('exa', unavailable, () => Promise.resolve(searchResult('exa')))) - await expect(b.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: 'perplexity' }) + await expect(b.web.search({ query: 'q' })).resolves.toMatchObject({ content: 'perplexity' }) }) it('runs the selected provider and returns its result', async () => { @@ -139,7 +138,6 @@ describe('WebService execution resolution', () => { searchResult('exa', { content: 'answer', sources: [{ url: 'https://a' }] }), ))) const result = await web.search({ query: 'q' }) - expect(result.providerId).toBe('exa') expect(result.content).toBe('answer') expect(result.sources).toEqual([{ url: 'https://a' }]) }) @@ -149,11 +147,11 @@ describe('WebService execution resolution', () => { const seen: (AbortSignal | undefined)[] = [] web.registerSearchProvider({ id: 'exa', - status: () => available, - search: (_request, exec) => { seen.push(exec?.signal); return Promise.resolve(searchResult('exa')) }, + available: () => available, + search: (_request, signal) => { seen.push(signal); return Promise.resolve(searchResult('exa')) }, }) const controller = new AbortController() - await web.search({ query: 'q' }, { signal: controller.signal }) + await web.search({ query: 'q' }, controller.signal) expect(seen[0]).toBe(controller.signal) }) }) @@ -195,7 +193,7 @@ describe('WebService fetch capability', () => { const { web } = await mountWeb() web.registerFetchProvider(makeFetchProvider('local-http', available, fetchResult('local-http'))) const result = await web.fetch({ url: 'https://example.com' }) - expect(result.providerId).toBe('local-http') + expect(result.body.content).toBe('local-http') expect(result.statusCode).toBe(200) }) diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index a219ea1b1d..c35d5340f7 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -133,7 +133,6 @@ { "doc": "docs/core-data-structures/web.md", "symbol": "WebFetchRequest", "source": "packages/web/web/src/types.ts" }, { "doc": "docs/core-data-structures/web.md", "symbol": "WebFetchResult", "source": "packages/web/web/src/types.ts" }, { "doc": "docs/core-data-structures/web.md", "symbol": "WebFetchBody", "source": "packages/web/web/src/types.ts" }, - { "doc": "docs/core-data-structures/web.md", "symbol": "WebProviderStatus", "source": "packages/web/web/src/types.ts" }, { "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowStartRequest", "source": "packages/workflow/workflow/src/types.ts" }, { "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowMeta", "source": "packages/workflow/workflow/src/types.ts" }, From a8d1624695b78677e65d287b87aea016f3de1288 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 04:26:35 +0800 Subject: [PATCH 07/21] docs: close web seam simplification RFC --- docs/rfc/INDEX.md | 2 +- .../2026-06-24-web-capability-seam.md | 2 +- .../2026-07-12-prune-unused-web-seam-fields.md | 17 ++++++----------- 3 files changed, 8 insertions(+), 13 deletions(-) rename docs/rfc/{proposed => implemented}/simplification/2026-07-12-prune-unused-web-seam-fields.md (61%) diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index b338edfc52..97f0f5c511 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -20,7 +20,6 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Unify the agent id and the session id](proposed/simplification/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 | | [Prune dead public and result surface](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 | | [Drop unconsumed skill provider events](proposed/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md) | 2026-07-12 | -| [Prune unused web seam fields](proposed/simplification/2026-07-12-prune-unused-web-seam-fields.md) | 2026-07-12 | | [Simplify session-log representation](proposed/simplification/2026-07-12-simplify-session-log-representation.md) | 2026-07-12 | ### Architecture @@ -100,6 +99,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Share the app bins' boot glue instead of maintaining twin copies](implemented/simplification/2026-07-04-share-app-bin-boot-glue.md) | 2026-07-04 | | [Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics](implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md) | 2026-07-04 | | [Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback](implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md) | 2026-07-04 | +| [Prune unused web seam fields](implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md) | 2026-07-12 | ### Architecture diff --git a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md b/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md index 0763a771f5..3d907bf334 100644 --- a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md +++ b/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md @@ -64,7 +64,7 @@ flowchart LR toolWeb -->|ctx.tools.register| webFetch["tool: web_fetch"] ``` -`@deepseek-ai/dsh-web` depends only on Cordis and low-level harness support. It declares `ctx.web`, provider interfaces, request/result types, the provider status type, and error codes. It does not import tool, agent, session, LLM, or provider packages. +`@deepseek-ai/dsh-web` depends only on Cordis and low-level harness support. It declares `ctx.web`, provider interfaces, request/result types, the provider availability contract, and error codes. It does not import tool, agent, session, LLM, or provider packages. Provider packages depend on `@deepseek-ai/dsh-web` and Cordis. They own credentials, endpoint config, provider-specific request mapping, provider-specific response parsing, and provider-specific error translation into `WebError`. They issue network requests with platform-native `fetch` at the repo's Node floor, mirroring `@deepseek-ai/dsh-llm-deepseek`'s adapter, NOT a cordis HTTP-client service (`ctx.http`/`@cordisjs/plugin-http`) — even where a Perplexity provider's request is shaped like an OpenAI-compatible chat completion, that wire shape is a provider-private detail and does not make the provider depend on `ctx.llm`. A provider does NOT own the `ctx.web` key (two search providers cannot both own it): like `dsh-llm-deepseek`, each provider package is a function/namespace plugin (`inject: ['web']`) whose `apply` constructs the backend and calls `ctx.web.registerSearchProvider` / `registerFetchProvider`. `@deepseek-ai/dsh-web` is the `export default` service that owns the key. diff --git a/docs/rfc/proposed/simplification/2026-07-12-prune-unused-web-seam-fields.md b/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md similarity index 61% rename from docs/rfc/proposed/simplification/2026-07-12-prune-unused-web-seam-fields.md rename to docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md index 1a8495426e..8ece4214b7 100644 --- a/docs/rfc/proposed/simplification/2026-07-12-prune-unused-web-seam-fields.md +++ b/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md @@ -1,6 +1,6 @@ # RFC: Prune unused web seam fields -Status: proposed +Status: implemented ## Problem @@ -8,23 +8,18 @@ The web capability carries request/result/status values that every shipped imple `WebFetchRequest.timeoutMs` is likewise never set by a production caller. `tool-web` supplies only the URL, uses the tool definition's timeout plus `exec.signal` for the caller deadline, and relies on the local provider's configured default as a backstop. The unused per-request override forces `web-fetch-local` to expose `maxTimeoutMs`, clamp two timeout sources, and document/test precedence no product path can select. `WebExecContext` is another one-field wrapper: every caller allocates `{ signal }` and every provider immediately unwraps `exec?.signal`; no second execution-control field exists. -## Proposal +## Decision -Remove the search/fetch `providerId` result echoes and search `query` echo; callers already own the request and provider selection. Shrink provider status to availability alone, preferably a boolean-returning method if that produces the clearest seam. Remove per-request fetch timeout, `maxTimeoutMs`, and their clamp/validation branches while retaining the provider's configurable default timeout and tool-level deadline. Replace `WebExecContext` with a direct optional `AbortSignal` parameter. +The web seam omits the search/fetch `providerId` result echoes and search `query` echo; callers already own the request and provider selection. Providers expose availability as a boolean-returning method. Fetch requests have no per-request timeout or `maxTimeoutMs` clamp; the local provider retains its configurable default timeout and the tool retains its own deadline. Provider methods receive a direct optional `AbortSignal` instead of a one-field `WebExecContext` wrapper. -Update all web implementations, the model-facing tool, package READMEs/JSDoc, type-equivalence records, and tests. Keep the interface/implementation/consumer package split, provider selection, source citations, final-URL/status data, truncation reporting, and all safety limits. +All web implementations and the model-facing tool use the smaller contract. The interface/implementation/consumer package split, provider selection, source citations, final-URL/status data, truncation reporting, and safety limits remain. ## Alternatives considered **Keep self-describing results, per-request deadlines, and an extensible execution-context object.** Result echoes can help generic telemetry, a request timeout can help trusted programmatic callers, and the wrapper leaves room for future controls. No such consumer/second field exists; carrying duplicate identity, a second deadline policy, and wrap/unwrap plumbing through every provider makes the current contract harder to implement and explain. If telemetry or per-call budget control arrives, it should define which deadline wins, where provider identity is observed, and whether multiple controls justify a context object. -## Acceptance criteria +## Consequences -- Every retained web request/result/status field has a production reader or is required to execute the provider request. -- Tool-visible search/fetch output, provider fallback, abort behavior, configured timeout backstop, truncation, and citations remain covered. -- No `maxTimeoutMs`, request-timeout precedence branch, or one-field execution-context wrapper remains. -- Typecheck, coverage, snapshots, doc-sync, module-graph verification, build, and hygiene pass. - -## Risks +Every retained web request/result field is consumed by production code or required to execute the provider request. Tool-visible search/fetch output, provider fallback, abort behavior, the configured timeout backstop, truncation, and citations remain covered without a request-timeout precedence branch or execution-context wrapper. Pre-release programmatic callers lose result provenance echoes and per-request fetch deadlines. The provider still has a deployment-configurable timeout and respects cancellation, so the simplification removes configurability rather than a safety bound. From 582a8ba2888cb5ed01d245a114ff3d5b6a06e10a Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 04:32:40 +0800 Subject: [PATCH 08/21] refactor: drop unconsumed skill provider events --- docs/cordis-catalog/events.md | 22 ------------------- docs/cordis-catalog/services.md | 2 +- docs/event-producer-consumer.md | 2 -- docs/rfc/INDEX.md | 2 +- .../feature/2026-07-05-skill-system.md | 2 +- ...2-drop-unconsumed-skill-provider-events.md | 19 ++++++---------- .../cordis/tool-cordis/src/api-catalog.ts | 12 ---------- packages/skill/skill/src/index.ts | 22 +------------------ 8 files changed, 11 insertions(+), 72 deletions(-) rename docs/rfc/{proposed => implemented}/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md (52%) diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index acbd39b8fa..f5ff9242b1 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -285,28 +285,6 @@ Awaited durability checkpoint. The agent loop awaits `ctx.sessions.flush(session Source: [`packages/core/session/src/index.ts:101`](../../packages/core/session/src/index.ts) -## `skill/*` - -### `skill/provider-added` — emit - -A skill provider became resolvable in the `ctx.skills` registry. Consumers can observe this instead of depending on Cordis plugin load order, which is concurrent for sibling plugins. - -```ts cordis-catalog -'skill/provider-added'(provider: SkillProvider): void -``` - -Source: [`packages/skill/skill/src/index.ts:131`](../../packages/skill/skill/src/index.ts) - -### `skill/provider-removed` — emit - -A skill provider left the registry because its plugin fiber was disposed. - -```ts cordis-catalog -'skill/provider-removed'(name: string): void -``` - -Source: [`packages/skill/skill/src/index.ts:137`](../../packages/skill/skill/src/index.ts) - ## `subagent/*` ### `subagent/end` — emit diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 0aea3f1589..e417d20fa5 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -231,7 +231,7 @@ async list(options: SkillLookupOptions = {}): Promise async get(name: string, options: SkillLookupOptions = {}): Promise ``` -Source: [`packages/skill/skill/src/index.ts:158`](../../packages/skill/skill/src/index.ts) +Source: [`packages/skill/skill/src/index.ts:141`](../../packages/skill/skill/src/index.ts) ## `ctx.subagents` — `SubagentService` diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 5b0a82673e..dea714031e 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -29,8 +29,6 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - | | `session/event` | `emit` | [`packages/core/session/src/index.ts:83`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:101`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | -| `skill/provider-added` | `emit` | [`packages/skill/skill/src/index.ts:131`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - | -| `skill/provider-removed` | `emit` | [`packages/skill/skill/src/index.ts:137`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:90`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:66`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:72`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index b338edfc52..ae82f8e695 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -19,7 +19,6 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; |---|---| | [Unify the agent id and the session id](proposed/simplification/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 | | [Prune dead public and result surface](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 | -| [Drop unconsumed skill provider events](proposed/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md) | 2026-07-12 | | [Prune unused web seam fields](proposed/simplification/2026-07-12-prune-unused-web-seam-fields.md) | 2026-07-12 | | [Simplify session-log representation](proposed/simplification/2026-07-12-simplify-session-log-representation.md) | 2026-07-12 | @@ -100,6 +99,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Share the app bins' boot glue instead of maintaining twin copies](implemented/simplification/2026-07-04-share-app-bin-boot-glue.md) | 2026-07-04 | | [Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics](implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md) | 2026-07-04 | | [Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback](implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md) | 2026-07-04 | +| [Drop unconsumed skill provider events](implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md) | 2026-07-12 | ### Architecture diff --git a/docs/rfc/implemented/feature/2026-07-05-skill-system.md b/docs/rfc/implemented/feature/2026-07-05-skill-system.md index b3007529cd..c29140a854 100644 --- a/docs/rfc/implemented/feature/2026-07-05-skill-system.md +++ b/docs/rfc/implemented/feature/2026-07-05-skill-system.md @@ -12,7 +12,7 @@ DeepSeek Harness uses the same primitive so project-specific review, plugin-auth `@deepseek-ai/dsh-skill` is the pure provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` is the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` owns the session-prefix catalog and model-facing loader tool. `dsh-agent-core` loads the registry, local provider, and consumer by default so stdio and ACP apps get the same behavior while embedded or remote providers contribute skills without changing the registry or consumer. Its `skills` config forwards `registry`, `local`, and `tool` branches to those owners. -Provider plugins register synchronously during `apply()`. Provider catalogs return ranked candidates from awaited `list()` calls, where remote providers perform initialization, authentication, and discovery while honoring the lookup abort signal. The registry validates each candidate, resolves same-name skills first-wins by rank, provider registration order, and provider-local order, then sorts summaries by skill name for deterministic consumers. It caches only completed catalog snapshots and retries when a provider/runtime revision changes during discovery, so an unload cannot freeze a stale, unresolvable skill into a session prefix. Runtime `ctx.skills.register(...)` remains a convenience for embedded in-process skills and uses project-over-user priority; `runtime` is reserved as the registry-owned provider name. +Provider plugins register synchronously during `apply()`. Provider membership is direct effect-owned state: registration and disposal invalidate completed catalogs synchronously, and discovery reads the current provider map on demand rather than observing registry-change events. Provider catalogs return ranked candidates from awaited `list()` calls, where remote providers perform initialization, authentication, and discovery while honoring the lookup abort signal. The registry validates each candidate, resolves same-name skills first-wins by rank, provider registration order, and provider-local order, then sorts summaries by skill name for deterministic consumers. It caches only completed catalog snapshots and retries when a provider/runtime revision changes during discovery, so an unload cannot freeze a stale, unresolvable skill into a session prefix. Runtime `ctx.skills.register(...)` remains a convenience for embedded in-process skills and uses project-over-user priority; `runtime` is reserved as the registry-owned provider name. The local provider scans cwd-sensitive project roots, custom roots, and user roots in first-wins rank order: project `.dsh`, project `.agents`, `customSkillDirs`, user `.dsh`, then user `.agents`. The user `.dsh/skills` scan skips `.system` so a system-owned directory is not treated as normal user content. DeepSeek Harness does not ship built-in system skills; embedded or remote providers supply additional skills when configured. diff --git a/docs/rfc/proposed/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md b/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md similarity index 52% rename from docs/rfc/proposed/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md rename to docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md index 9a0d9d2cb7..0907a63417 100644 --- a/docs/rfc/proposed/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md +++ b/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md @@ -1,6 +1,6 @@ # RFC: Drop unconsumed skill provider events -Status: proposed +Status: implemented ## Problem @@ -10,23 +10,18 @@ Skill discovery reads the current provider map on demand, provider registration `tools/change` and `system-prompt/change` are explicitly outside this proposal. Existing simplification decisions retain them as intentional observation points for live tool and prompt UIs, and self-referential mounted plugins already use `tools/change`. This proposal also leaves `subagent/provider-added`/`removed` unchanged because `tool-subagent` has a production lifecycle consumer. -## Proposal +## Decision -Delete the two skill-provider declarations and every emit path, rollback-order branch, test, and generated catalog/matrix row that exists only for them. Remove the corresponding skill-registry README/JSDoc contract. Where tests used an event to observe cleanup, assert provider lookup or collected output instead. +The skill registry declares and emits no provider-membership events. Provider registration and disposal remain direct effect-owned state changes that synchronously invalidate completed catalogs; lookup and discovery read the current provider map on demand. Tests observe cleanup through provider lookup and collected output rather than lifecycle notifications. -Amend the skill-system RFC and package documentation so provider registration is described as direct effect-owned state with cache invalidation, not as a lifecycle notification contract. +The generated event catalog, API catalog, and producer/consumer matrix omit the deleted notifications. The skill-system RFC and package documentation describe registration through its direct effect-owned state and cache-invalidation contract. ## Alternatives considered **Keep skill-provider notifications for future plugins.** A third-party plugin could observe provider availability, but direct provider registration and on-demand lookup are the extension contract; no current consumer needs a push signal. If a future sibling-load race appears, it can introduce a notification with the identity and readiness semantics that consumer requires, as the subagent registry did. -## Acceptance criteria +## Consequences -- The generated event matrix contains no row for `skill/provider-added` or `skill/provider-removed`. -- Skill discovery, direct runtime registration, provider effect rollback/disposal, cache invalidation, and registry lookup cleanup behave unchanged; listener-triggered rollback disappears with the events. -- `tools/change`, `system-prompt/change`, and the real subagent provider lifecycle consumer remain documented and covered. -- Typecheck, coverage, snapshots, doc-sync, module-graph verification, build, and hygiene pass. +The generated event matrix contains no row for `skill/provider-added` or `skill/provider-removed`. Skill discovery, direct runtime registration, provider effect rollback/disposal, cache invalidation, and registry lookup cleanup remain; listener-triggered rollback disappears with the events. `tools/change`, `system-prompt/change`, and the consumed subagent provider lifecycle events are unchanged. -## Risks - -This removes pre-release skill-provider observation points while retaining both ways third-party plugins contribute skills: direct runtime registration and provider registration. A future consumer that needs live provider availability must add a purpose-built notification rather than relying on these generic events. +Pre-release consumers lose skill-provider observation points while retaining both ways to contribute skills: direct runtime registration and provider registration. A future consumer that needs live provider availability must add a purpose-built notification with the identity and readiness semantics it actually requires. diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index b4420e80c7..db54a1924b 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -368,18 +368,6 @@ export const EVENT_API: readonly EventApiEntry[] = [ signature: '\'session/flush\'(this: Scoped, session: Session): Promise | void', summary: 'Awaited durability checkpoint.', }, - { - name: 'skill/provider-added', - mode: 'emit', - signature: '\'skill/provider-added\'(provider: SkillProvider): void', - summary: 'A skill provider became resolvable in the `ctx.skills` registry.', - }, - { - name: 'skill/provider-removed', - mode: 'emit', - signature: '\'skill/provider-removed\'(name: string): void', - summary: 'A skill provider left the registry because its plugin fiber was disposed.', - }, { name: 'subagent/end', mode: 'emit', diff --git a/packages/skill/skill/src/index.ts b/packages/skill/skill/src/index.ts index 5ef3f78465..53f291c9dd 100644 --- a/packages/skill/skill/src/index.ts +++ b/packages/skill/skill/src/index.ts @@ -119,23 +119,6 @@ declare module 'cordis' { interface Context { skills: SkillService } - - interface Events { - /** - * A skill provider became resolvable in the `ctx.skills` registry. - * Consumers can observe this instead of depending on Cordis plugin load - * order, which is concurrent for sibling plugins. - * @param provider - the provider that just registered. - * @mode emit - */ - 'skill/provider-added'(provider: SkillProvider): void - /** - * A skill provider left the registry because its plugin fiber was disposed. - * @param name - the registry name that no longer resolves. - * @mode emit - */ - 'skill/provider-removed'(name: string): void - } } interface IndexedCandidate { @@ -196,19 +179,16 @@ export class SkillService extends Service { throw new Error(`a skill provider named "${name}" is already registered`) } const providers = this.providers - const ctx = this.ctx const order = this.nextProviderOrder const invalidateCache = (): void => { this.invalidateCache() } this.nextProviderOrder += 1 - const dispose = ctx.effect(function* () { + const dispose = this.ctx.effect(function* () { providers.set(name, { provider, order }) invalidateCache() yield () => { providers.delete(name) invalidateCache() - ctx.emit('skill/provider-removed', name) } - ctx.emit('skill/provider-added', provider) }, 'skills.registerProvider()') // eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity return dispose From 0815ff4db4857447de9da581a26cd0d8a61c160d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:00:54 +0800 Subject: [PATCH 09/21] refactor: share loader smoke harness --- docs/config-catalog.md | 1 + docs/module-graph.md | 2 + examples/AGENTS.md | 2 +- .../tests/code-mode-keyless-smoke.e2e.ts | 102 ++------------ .../coding-agent/tests/keyless-smoke.e2e.ts | 116 +++------------- .../cordis-agent/tests/keyless-smoke.e2e.ts | 105 ++------------ examples/echo-agent/tests/echo.e2e.ts | 130 +++--------------- knip.json | 5 + packages/README.md | 2 +- packages/support/README.md | 3 +- packages/support/loader-smoke/README.md | 7 + packages/support/loader-smoke/package.json | 33 +++++ packages/support/loader-smoke/src/index.ts | 117 ++++++++++++++++ .../loader-smoke/tests/fixtures/fail.ts | 4 + .../loader-smoke/tests/fixtures/hang.ts | 4 + .../loader-smoke/tests/fixtures/success.ts | 16 +++ .../loader-smoke/tests/loader-smoke.spec.ts | 61 ++++++++ packages/support/loader-smoke/tsconfig.json | 11 ++ pnpm-lock.yaml | 10 ++ tsconfig.build.json | 1 + tsconfig.json | 1 + 21 files changed, 344 insertions(+), 389 deletions(-) create mode 100644 packages/support/loader-smoke/README.md create mode 100644 packages/support/loader-smoke/package.json create mode 100644 packages/support/loader-smoke/src/index.ts create mode 100644 packages/support/loader-smoke/tests/fixtures/fail.ts create mode 100644 packages/support/loader-smoke/tests/fixtures/hang.ts create mode 100644 packages/support/loader-smoke/tests/fixtures/success.ts create mode 100644 packages/support/loader-smoke/tests/loader-smoke.spec.ts create mode 100644 packages/support/loader-smoke/tsconfig.json diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 9d4b8038b5..cba82ef962 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1205,6 +1205,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts)) - `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts)) - `@deepseek-ai/dsh-jsonrpc-agent` ([`packages/ui/jsonrpc-agent/src/index.ts`](../packages/ui/jsonrpc-agent/src/index.ts)) +- `@deepseek-ai/dsh-loader-smoke` ([`packages/support/loader-smoke/src/index.ts`](../packages/support/loader-smoke/src/index.ts)) - `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) - `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts)) - `@deepseek-ai/dsh-subagent-subprocess` ([`packages/subagent/subagent-subprocess/src/index.ts`](../packages/subagent/subagent-subprocess/src/index.ts)) diff --git a/docs/module-graph.md b/docs/module-graph.md index e4d0b3367d..9a0ac4cbde 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -86,6 +86,7 @@ flowchart TD pkg_acp_snapshot["acp-snapshot"] pkg_invariants["invariants"] pkg_llm_replay["llm-replay"] + pkg_loader_smoke["loader-smoke"] pkg_subagent_mock["subagent-mock"] end subgraph group_ui["packages/ui"] @@ -329,6 +330,7 @@ flowchart TD | [`skill`](../packages/skill/skill) | `skill` | — | | [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | — | | [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | — | +| [`loader-smoke`](../packages/support/loader-smoke) | `support` | — | | [`app-boot`](../packages/ui/app-boot) | `ui` | — | | [`jsonrpc-agent`](../packages/ui/jsonrpc-agent) | `ui` | — | | [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — | diff --git a/examples/AGENTS.md b/examples/AGENTS.md index d54104a189..cfc1b07604 100644 --- a/examples/AGENTS.md +++ b/examples/AGENTS.md @@ -13,7 +13,7 @@ Each example must have **both** kinds of end-to-end smoke, because they catch di **Exception — keyless-by-nature examples.** An example whose model is itself a mock/deterministic stand-in (no real provider) has no meaningful with-key smoke; the keyless smoke is the complete requirement. State the exception inline in the test. -A keyless smoke that spawns the example from a temp cwd must set `TSX_TSCONFIG_PATH` to the repo-root tsconfig (the unbuilt `paths` map is found by searching UP from cwd), and pass `--expose-internals` when the `cordis.yml` loads the HMR plugin (mirror the `demo:*` script). +A keyless stdio smoke uses `@deepseek-ai/dsh-loader-smoke`, which owns the isolated cwd and DSH homes, repo tsconfig pin, `--expose-internals`, subprocess deadline, EOF, captured diagnostics, forced kill, and cleanup. The example test supplies only its absolute bin/config/tsconfig paths, environment overrides, stdin lines, and output assertions. ## Current state diff --git a/examples/coding-agent/tests/code-mode-keyless-smoke.e2e.ts b/examples/coding-agent/tests/code-mode-keyless-smoke.e2e.ts index 718aa96721..ac2cb9430b 100644 --- a/examples/coding-agent/tests/code-mode-keyless-smoke.e2e.ts +++ b/examples/coding-agent/tests/code-mode-keyless-smoke.e2e.ts @@ -1,99 +1,27 @@ -import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { afterEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' +import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' /** - * Keyless Loader-path smoke for the Code Mode overlay: boot the REAL - * example through the `@deepseek-ai/dsh-stdio-agent` bin against - * `code-mode.cordis.yml` (the cordis Loader, `unwrapExports`, the include - * patches over ./cordis.yml, the worker-thread code runtime, and the - * registry in `mode: code`), then close stdin with no prompt and assert - * the Code Mode banner + a clean exit. - * - * No prompt is ever sent, so the model is NEVER called and no `run_code` - * turn happens — a dummy key lets `llm-deepseek`'s key-PRESENT check boot - * the tree. This is the export-shape guard (postmortem 0001) for the Code - * Mode composition; the with-key proof lives in `code-mode.e2e.ts`. + * Keyless Loader-path smoke for the Code Mode overlay: boot the real include + * tree through stdio-agent and `code-mode.cordis.yml`, then close stdin without + * a prompt and assert the banner. No model or `run_code` turn runs. */ const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url)) const configPath = fileURLToPath(new URL('../code-mode.cordis.yml', import.meta.url)) -const tsxLoader = fileURLToPath(import.meta.resolve('tsx')) -// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig -// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside -// the repo, so point it at the repo tsconfig. -const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) -// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader -// startup can therefore outlive a tight smoke-test deadline before the child -// emits any output; 30s still detects a wedged process without confusing -// bounded CI contention with a lifecycle failure. -const PROCESS_TIMEOUT_MS = 30_000 -// Leave enough room for the process-owned timeout to report captured output -// before Vitest aborts the test itself. -const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000 - -let child: ChildProcessWithoutNullStreams | undefined -let workdir: string | undefined - -afterEach(async () => { - if (child !== undefined && child.exitCode === null) child.kill('SIGKILL') - child = undefined - if (workdir !== undefined) await rm(workdir, { recursive: true, force: true }) - workdir = undefined -}) - -async function bootAndEof(): Promise<{ stdout: string; code: number }> { - workdir = await mkdtemp(join(tmpdir(), 'code-mode-smoke-')) - const cwd = workdir - return new Promise((resolve, reject) => { - const proc = spawn( - process.execPath, - // --expose-internals: the included cordis.yml loads the HMR plugin (mirrors demo:code-mode). - ['--expose-internals', '--import', tsxLoader, binScript, configPath], - { - cwd, - env: { - ...process.env, - TSX_TSCONFIG_PATH: repoTsconfig, - // A dummy key so llm-deepseek's apply() (key-PRESENT check only) boots. - // No prompt is sent, so the adapter never streams — no network call. - DEEPSEEK_API_KEY: 'keyless-smoke-no-call', - }, - stdio: ['pipe', 'pipe', 'pipe'], - }, - ) - child = proc - let stdout = '' - let stderr = '' - proc.stdout.setEncoding('utf8') - proc.stdout.on('data', (chunk: string) => { stdout += chunk }) - proc.stderr.setEncoding('utf8') - proc.stderr.on('data', (chunk: string) => { stderr += chunk }) - - const timer = setTimeout(() => { - proc.kill('SIGKILL') - reject(new Error(`code-mode overlay did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`)) - }, PROCESS_TIMEOUT_MS) - - proc.on('exit', (code) => { - clearTimeout(timer) - if (code === 0) resolve({ stdout, code }) - else reject(new Error(`code-mode overlay exited ${code}. stderr:\n${stderr}`)) - }) - proc.on('error', (err) => { clearTimeout(timer); reject(err) }) - - // No prompt — just EOF, so the stdio UI exits without ever running a turn. - proc.stdin.end() - }) -} +const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) describe('code-mode overlay keyless smoke (real code-mode.cordis.yml via the Loader)', () => { it('boots the Code Mode plugin tree, prints its banner, and exits cleanly on EOF', async () => { - const { stdout, code } = await bootAndEof() - expect(code).toBe(0) + const { stdout } = await runLoaderSmoke({ + label: 'code-mode overlay', + tempDirPrefix: 'code-mode-smoke-', + binScript, + configPath, + tsconfigPath, + env: { DEEPSEEK_API_KEY: 'keyless-smoke-no-call' }, + }) expect(stdout).toContain('code-mode agent ready.') - }, TEST_TIMEOUT_MS) + }, LOADER_SMOKE_TEST_TIMEOUT_MS) }) diff --git a/examples/coding-agent/tests/keyless-smoke.e2e.ts b/examples/coding-agent/tests/keyless-smoke.e2e.ts index ea223bbad4..6cfeca646e 100644 --- a/examples/coding-agent/tests/keyless-smoke.e2e.ts +++ b/examples/coding-agent/tests/keyless-smoke.e2e.ts @@ -1,112 +1,28 @@ -import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { afterEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' +import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' /** - * Keyless Loader-path smoke for examples/coding-agent: boot the REAL example - * through the `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` (the - * cordis Loader, `unwrapExports`, the full plugin tree incl. the - * `@deepseek-ai/dsh-agent-core` bundle and the app's in-package readline UI - * module), then close stdin with no prompt and assert the - * ready banner + a clean exit. - * - * No prompt is ever sent, so the model is NEVER called — this is why it runs - * without a real key. coding-agent's `cordis.yml` loads `llm-deepseek`, whose - * `apply()` only requires a key to be PRESENT (it does not validate it and only - * uses it when a stream actually starts), so a dummy key lets the tree boot - * while the absence of any prompt guarantees no network call. The value is the - * real-Loader-path guard that the composed tree boots (see postmortem 0001; - * the app carries no `inject`, so its export SHAPE is pinned by the stdio-agent - * unit suite's unwrap assertion, not by a crash here), - * complementing coding-agent's with-key e2e suites which prove the real - * product. + * Keyless Loader-path smoke for examples/coding-agent: boot the real example + * through the stdio-agent bin and its `cordis.yml`, then close stdin without a + * prompt and assert the banner. The dummy key satisfies adapter construction; + * immediate EOF guarantees there is no model call. */ -// TODO(loader-smoke-harness): extract the shared spawn/tempdir/timeout/EOF -// harness used here, code-mode-keyless-smoke, and cordis-agent's keyless smoke. -// The dsh-stdio-agent bin (the demo:repl entry) and this example's cordis.yml. -// The bin resolves its config-path arg from CWD; the test spawns from a temp -// cwd, so we pass the example config's ABSOLUTE path. const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url)) const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url)) -const tsxLoader = fileURLToPath(import.meta.resolve('tsx')) -// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig -// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside -// the repo, so point it at the repo tsconfig (root is four levels up). -const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) -// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader -// startup can therefore outlive a tight smoke-test deadline before the child -// emits any output; 30s still detects a wedged process without confusing -// bounded CI contention with a lifecycle failure. -const PROCESS_TIMEOUT_MS = 30_000 -// Leave enough room for the process-owned timeout to report captured output -// before Vitest aborts the test itself. -const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000 - -let child: ChildProcessWithoutNullStreams | undefined -let workdir: string | undefined - -afterEach(async () => { - if (child !== undefined && child.exitCode === null) child.kill('SIGKILL') - child = undefined - if (workdir !== undefined) await rm(workdir, { recursive: true, force: true }) - workdir = undefined -}) - -async function bootAndEof(): Promise<{ stdout: string; code: number }> { - workdir = await mkdtemp(join(tmpdir(), 'coding-smoke-')) - const cwd = workdir - return new Promise((resolve, reject) => { - const proc = spawn( - process.execPath, - // --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:repl). - ['--expose-internals', '--import', tsxLoader, binScript, configPath], - { - cwd, - env: { - ...process.env, - TSX_TSCONFIG_PATH: repoTsconfig, - // A dummy key so llm-deepseek's apply() (key-PRESENT check only) boots. - // No prompt is sent, so the adapter never streams — no network call. - DEEPSEEK_API_KEY: 'keyless-smoke-no-call', - DSH_HOME: join(cwd, '.dsh'), - DSH_AGENTS_HOME: join(cwd, '.agents'), - }, - stdio: ['pipe', 'pipe', 'pipe'], - }, - ) - child = proc - let stdout = '' - let stderr = '' - proc.stdout.setEncoding('utf8') - proc.stdout.on('data', (chunk: string) => { stdout += chunk }) - proc.stderr.setEncoding('utf8') - proc.stderr.on('data', (chunk: string) => { stderr += chunk }) - - const timer = setTimeout(() => { - proc.kill('SIGKILL') - reject(new Error(`coding-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`)) - }, PROCESS_TIMEOUT_MS) - - proc.on('exit', (code) => { - clearTimeout(timer) - if (code === 0) resolve({ stdout, code }) - else reject(new Error(`coding-agent exited ${code}. stderr:\n${stderr}`)) - }) - proc.on('error', (err) => { clearTimeout(timer); reject(err) }) - - // No prompt — just EOF, so the stdio UI exits without ever running a turn. - proc.stdin.end() - }) -} +const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) describe('coding-agent keyless smoke (real cordis.yml via the Loader)', () => { it('boots the full plugin tree, prints its banner, and exits cleanly on EOF', async () => { - const { stdout, code } = await bootAndEof() - expect(code).toBe(0) + const { stdout } = await runLoaderSmoke({ + label: 'coding-agent', + tempDirPrefix: 'coding-smoke-', + binScript, + configPath, + tsconfigPath, + env: { DEEPSEEK_API_KEY: 'keyless-smoke-no-call' }, + }) expect(stdout).toContain('agent REPL ready.') - }, TEST_TIMEOUT_MS) + }, LOADER_SMOKE_TEST_TIMEOUT_MS) }) diff --git a/examples/cordis-agent/tests/keyless-smoke.e2e.ts b/examples/cordis-agent/tests/keyless-smoke.e2e.ts index d09cdf4728..c1f20f1987 100644 --- a/examples/cordis-agent/tests/keyless-smoke.e2e.ts +++ b/examples/cordis-agent/tests/keyless-smoke.e2e.ts @@ -1,102 +1,27 @@ -import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { afterEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' +import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' /** - * Keyless Loader-path smoke for examples/cordis-agent: boot the REAL example - * through the `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` — - * the cordis Loader, `unwrapExports`, the full plugin tree INCLUDING the - * `@deepseek-ai/dsh-tool-cordis` package resolved by name (whose `inject` - * would crash a collapsed export shape at load, see docs/postmortem/0001) — - * then close stdin with no prompt and assert the ready banner + a clean exit. - * - * No prompt is ever sent, so the model is NEVER called — that is why it runs - * without a real key: `llm-deepseek`'s apply() only requires a key to be - * PRESENT, and the absence of any prompt guarantees no network call. The - * with-key product proof lives in cordis-tools.e2e.ts. + * Keyless Loader-path smoke for examples/cordis-agent: boot the real tree, + * including tool-cordis resolved by package name, then close stdin without a + * prompt and assert the banner. The dummy key never reaches a model call. */ -// The dsh-stdio-agent bin (the demo:cordis entry) and this example's cordis.yml. -// The bin resolves its config-path arg from CWD; the test spawns from a temp -// cwd, so we pass the example config's ABSOLUTE path. const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url)) const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url)) -const tsxLoader = fileURLToPath(import.meta.resolve('tsx')) -// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig -// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside -// the repo, so point it at the repo tsconfig (root is three levels up). -const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) -// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader -// startup can therefore outlive a tight smoke-test deadline before the child -// emits any output; 30s still detects a wedged process without confusing -// bounded CI contention with a lifecycle failure. -const PROCESS_TIMEOUT_MS = 30_000 -// Leave enough room for the process-owned timeout to report captured output -// before Vitest aborts the test itself. -const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000 - -let child: ChildProcessWithoutNullStreams | undefined -let workdir: string | undefined - -afterEach(async () => { - if (child !== undefined && child.exitCode === null) child.kill('SIGKILL') - child = undefined - if (workdir !== undefined) await rm(workdir, { recursive: true, force: true }) - workdir = undefined -}) - -async function bootAndEof(): Promise<{ stdout: string; code: number }> { - workdir = await mkdtemp(join(tmpdir(), 'cordis-smoke-')) - const cwd = workdir - return new Promise((resolve, reject) => { - const proc = spawn( - process.execPath, - // --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:cordis). - ['--expose-internals', '--import', tsxLoader, binScript, configPath], - { - cwd, - env: { - ...process.env, - TSX_TSCONFIG_PATH: repoTsconfig, - // A dummy key so llm-deepseek's apply() (key-PRESENT check only) boots. - // No prompt is sent, so the adapter never streams — no network call. - DEEPSEEK_API_KEY: 'keyless-smoke-no-call', - }, - stdio: ['pipe', 'pipe', 'pipe'], - }, - ) - child = proc - let stdout = '' - let stderr = '' - proc.stdout.setEncoding('utf8') - proc.stdout.on('data', (chunk: string) => { stdout += chunk }) - proc.stderr.setEncoding('utf8') - proc.stderr.on('data', (chunk: string) => { stderr += chunk }) - - const timer = setTimeout(() => { - proc.kill('SIGKILL') - reject(new Error(`cordis-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`)) - }, PROCESS_TIMEOUT_MS) - - proc.on('exit', (code) => { - clearTimeout(timer) - if (code === 0) resolve({ stdout, code }) - else reject(new Error(`cordis-agent exited ${code}. stderr:\n${stderr}`)) - }) - proc.on('error', (err) => { clearTimeout(timer); reject(err) }) - - // No prompt — just EOF, so the stdio UI exits without ever running a turn. - proc.stdin.end() - }) -} +const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) describe('cordis-agent keyless smoke (real cordis.yml via the Loader)', () => { it('boots the full plugin tree incl. tool-cordis, prints its banner, and exits cleanly on EOF', async () => { - const { stdout, code } = await bootAndEof() - expect(code).toBe(0) + const { stdout } = await runLoaderSmoke({ + label: 'cordis-agent', + tempDirPrefix: 'cordis-smoke-', + binScript, + configPath, + tsconfigPath, + env: { DEEPSEEK_API_KEY: 'keyless-smoke-no-call' }, + }) expect(stdout).toContain('cordis-agent ready.') - }, TEST_TIMEOUT_MS) + }, LOADER_SMOKE_TEST_TIMEOUT_MS) }) diff --git a/examples/echo-agent/tests/echo.e2e.ts b/examples/echo-agent/tests/echo.e2e.ts index 5db02fb6be..1bd2733d94 100644 --- a/examples/echo-agent/tests/echo.e2e.ts +++ b/examples/echo-agent/tests/echo.e2e.ts @@ -1,131 +1,43 @@ -import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { afterEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' +import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' /** - * Keyless Loader-path smoke for examples/echo-agent: boot the REAL example - * through the `@deepseek-ai/dsh-stdio-agent` bin against this example's - * `cordis.yml` (the cordis Loader, `unwrapExports`, the whole plugin tree), - * pipe a script of stdin lines, and assert the rendered stdout. - * - * This is the guard the per-file unit suite structurally cannot be: it drives - * the `@deepseek-ai/dsh-stdio-agent` app plugin, the `@deepseek-ai/dsh-agent-core` - * bundle it loads, the app's in-package readline UI module, AND the - * example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path - * (see docs/postmortem/0001). The app itself carries no `inject`, so a stray - * `export default` would boot rather than crash here — the export SHAPE is - * pinned by the explicit unwrap assertion in the stdio-agent unit suite; this - * smoke proves the composed tree actually runs. It needs no API key — the - * `mock-echo` adapter never touches the network — so it runs in the default e2e - * gate. - * - * Both branches of mock-llm.ts are exercised: an `echo …` line (the tool - * round-trip → `ECHO: …`) and a plain line (the direct canned reply). + * Keyless-by-nature Loader-path coverage for examples/echo-agent. The real + * tree uses its deterministic mock model, so this suite is both the boot smoke + * and the complete behavior proof for the example. */ -// The dsh-stdio-agent bin (the demo:echo entry) and this example's cordis.yml. -// The bin resolves its config-path arg from CWD; the test spawns from a temp -// cwd, so we pass the example config's ABSOLUTE path. const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url)) const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url)) -const tsxLoader = fileURLToPath(import.meta.resolve('tsx')) -// Dev/test run UNBUILT: `@deepseek-ai/dsh-*` imports resolve through the root -// tsconfig `paths` map, which tsx finds by searching UP from cwd. We spawn from -// a temp cwd OUTSIDE the repo, so point tsx at the repo tsconfig explicitly -// (repo root is four levels up from examples/echo-agent/tests). -const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) -// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader -// startup can therefore outlive a tight smoke-test deadline before the child -// emits any output; 30s still detects a wedged process without confusing -// bounded CI contention with a lifecycle failure. -const PROCESS_TIMEOUT_MS = 30_000 -// Leave enough room for the process-owned timeout to report captured output -// before Vitest aborts the test itself. -const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000 +const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) -let child: ChildProcessWithoutNullStreams | undefined -let workdir: string | undefined - -afterEach(async () => { - if (child !== undefined && child.exitCode === null) child.kill('SIGKILL') - child = undefined - if (workdir !== undefined) await rm(workdir, { recursive: true, force: true }) - workdir = undefined -}) - -/** - * Boot echo-agent, write `lines` to its stdin, close stdin, and resolve with - * the full stdout once the process exits (the stdio UI exits on EOF after the - * agent settles). Rejects on a non-zero exit or the process deadline. - */ -async function runEcho(lines: string[]): Promise<{ stdout: string; code: number }> { - workdir = await mkdtemp(join(tmpdir(), 'echo-smoke-')) - const cwd = workdir - return new Promise((resolve, reject) => { - const proc = spawn( - process.execPath, - // --expose-internals: the example's cordis.yml loads the HMR plugin, which - // requires it (mirrors the `demo:echo` script). The whole point is to boot - // the example EXACTLY as it really runs, through the bin + Loader. - ['--expose-internals', '--import', tsxLoader, binScript, configPath], - { - cwd, - env: { - ...process.env, - TSX_TSCONFIG_PATH: repoTsconfig, - DSH_HOME: join(cwd, '.dsh'), - DSH_AGENTS_HOME: join(cwd, '.agents'), - }, - stdio: ['pipe', 'pipe', 'pipe'], - }, - ) - child = proc - let stdout = '' - let stderr = '' - proc.stdout.setEncoding('utf8') - proc.stdout.on('data', (chunk: string) => { stdout += chunk }) - proc.stderr.setEncoding('utf8') - proc.stderr.on('data', (chunk: string) => { stderr += chunk }) - - const timer = setTimeout(() => { - proc.kill('SIGKILL') - reject(new Error(`echo-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`)) - }, PROCESS_TIMEOUT_MS) - - proc.on('exit', (code) => { - clearTimeout(timer) - if (code === 0) resolve({ stdout, code }) - else reject(new Error(`echo-agent exited ${code}. stderr:\n${stderr}`)) - }) - proc.on('error', (err) => { clearTimeout(timer); reject(err) }) - - // Feed the script, then EOF so the stdio UI exits after the agent settles. - for (const line of lines) proc.stdin.write(`${line}\n`) - proc.stdin.end() +async function runEcho(stdinLines: readonly string[]): Promise { + const { stdout } = await runLoaderSmoke({ + label: 'echo-agent', + tempDirPrefix: 'echo-smoke-', + binScript, + configPath, + tsconfigPath, + stdinLines, }) + return stdout } describe('echo-agent keyless smoke (real cordis.yml via the Loader)', () => { it('boots, prints its welcome banner, and exits cleanly on stdin EOF', async () => { - const { stdout, code } = await runEcho([]) - expect(code).toBe(0) - expect(stdout).toContain('echo-agent ready.') - }, TEST_TIMEOUT_MS) + expect(await runEcho([])).toContain('echo-agent ready.') + }, LOADER_SMOKE_TEST_TIMEOUT_MS) it('runs the echo tool round-trip for an "echo …" line', async () => { - const { stdout } = await runEcho(['echo hello world']) - // mock-llm.ts emits a tool-call for the echo tool; echo-tool.ts uppercases. + const stdout = await runEcho(['echo hello world']) expect(stdout).toContain('[tool call] echo') expect(stdout).toContain('[tool result] ECHO: HELLO WORLD') - }, TEST_TIMEOUT_MS) + }, LOADER_SMOKE_TEST_TIMEOUT_MS) it('streams a direct canned reply for a non-echo line', async () => { - const { stdout } = await runEcho(['just chatting']) - // The direct-response branch of mock-llm.ts quotes the input back. + const stdout = await runEcho(['just chatting']) expect(stdout).toContain('just chatting') expect(stdout).not.toContain('[tool call]') - }, TEST_TIMEOUT_MS) + }, LOADER_SMOKE_TEST_TIMEOUT_MS) }) diff --git a/knip.json b/knip.json index 825980f205..77f5c05f2d 100644 --- a/knip.json +++ b/knip.json @@ -41,6 +41,11 @@ "project": ["src/**/*.ts", "tests/**/*.ts"], "ignoreDependencies": ["cordis"] }, + "packages/support/loader-smoke": { + "entry": ["tests/**/*.spec.ts", "tests/fixtures/*.ts"], + "project": ["src/**/*.ts", "tests/**/*.ts"], + "ignoreDependencies": ["cordis"] + }, "packages/core/agent-loop": { "entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"], "project": ["src/**/*.ts", "tests/**/*.ts"] diff --git a/packages/README.md b/packages/README.md index 7b6b1bb32a..f060b59163 100644 --- a/packages/README.md +++ b/packages/README.md @@ -26,7 +26,7 @@ Packages are grouped by modular role at `packages///`. The group dir | [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface | | [`session-persistence/`](session-persistence/README.md) | Persistence capability family: the seam + JSONL/SQLite backends | Product — stable surface | | [`ui/`](ui/README.md) | Editor/client integration surfaces: ACP bridge, JSON-RPC SDK server, app packages, user-approval and user-interaction seams, ask-user tool | Product — stable surface | -| [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, replay adapter, subagent mock) | Support — lower compatibility expectations | +| [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, replay, Loader-smoke, and subagent test helpers) | Support — lower compatibility expectations | | [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (the `Branded` primitive) | Support — small, stable, harness-dep-free | The split is the point: a package's group says whether it is part of the product API or support/test/example infrastructure, so release and removal decisions do not treat every package as an equal public contract. New packages join an existing group; adding a new top-level group is a deliberate act (extend the group READMEs and this table). diff --git a/packages/support/README.md b/packages/support/README.md index c8883a89ff..32ca292e4e 100644 --- a/packages/support/README.md +++ b/packages/support/README.md @@ -6,7 +6,8 @@ Packages that exist to serve development, testing, and the examples rather than |---|---|---| | `acp-snapshot/` | ACP snapshot suite kit: subprocess scenario harness + golden normalizers + the `defineAcpSnapshotSuite` factory | (library — imported by example `*.snapshot.ts` suites) | | `invariants/` | Dev-mode event-contract assertions | (listens on `session/*`, `agent/*`) | +| `loader-smoke/` | Shared real-Loader subprocess harness for keyless example smokes | (library — imported by example e2e suites) | | `llm-replay/` | Record/replay adapter: short-circuits `llm/stream` from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) | | `subagent-mock/` | Scripted `SubagentProvider` for deterministic seam/tool tests | (registers on `ctx.subagents`) | -`invariants` runs only in dev mode (contract checks, not runtime behavior). `llm-replay` backs the demos and the snapshot test tier under the per-file coverage gate. `acp-snapshot` carries the snapshot tier's harness/normalizer/suite machinery so every example's suite is a scenario table over one shared, gate-covered implementation. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers. +`invariants` runs only in dev mode (contract checks, not runtime behavior). `llm-replay` backs the demos and the snapshot test tier under the per-file coverage gate. `acp-snapshot` carries the snapshot tier's harness/normalizer/suite machinery, while `loader-smoke` owns the parallel stdio/Loader process boundary used by keyless example e2e suites. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers. diff --git a/packages/support/loader-smoke/README.md b/packages/support/loader-smoke/README.md new file mode 100644 index 0000000000..4901924a73 --- /dev/null +++ b/packages/support/loader-smoke/README.md @@ -0,0 +1,7 @@ +# `@deepseek-ai/dsh-loader-smoke` + +Shared subprocess harness for keyless example smokes that boot the real stdio-agent bin and a real `cordis.yml` through the Cordis Loader. A test supplies absolute bin/config/tsconfig paths, optional environment overrides, and stdin lines; `runLoaderSmoke` owns the isolated cwd, DSH homes, tsx path resolution, 30-second process deadline, captured diagnostics, forced kill, EOF, and cleanup. + +Successful runs return stdout and stderr only after a zero exit. Non-zero exits and deadlines reject with both captured streams. `LOADER_SMOKE_TEST_TIMEOUT_MS` leaves Vitest enough room for the process-owned diagnostic timeout to fire first. + +This is support-tier test infrastructure, not product API. The consumers are the Loader-path smokes under `examples/{echo-agent,coding-agent,cordis-agent}`. diff --git a/packages/support/loader-smoke/package.json b/packages/support/loader-smoke/package.json new file mode 100644 index 0000000000..ddba421b41 --- /dev/null +++ b/packages/support/loader-smoke/package.json @@ -0,0 +1,33 @@ +{ + "name": "@deepseek-ai/dsh-loader-smoke", + "description": "Shared subprocess harness for keyless real-Loader example smoke tests", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "dependencies": { + "tsx": "^4.22.4" + }, + "peerDependencies": { + "cordis": "^4.0.0-rc.6" + }, + "devDependencies": { + "cordis": "^4.0.0-rc.6" + } +} diff --git a/packages/support/loader-smoke/src/index.ts b/packages/support/loader-smoke/src/index.ts new file mode 100644 index 0000000000..72839c6a05 --- /dev/null +++ b/packages/support/loader-smoke/src/index.ts @@ -0,0 +1,117 @@ +/** + * Shared subprocess harness for keyless example smokes that boot a real + * `cordis.yml` through the stdio-agent bin and Cordis Loader. + * + * @module @deepseek-ai/dsh-loader-smoke + */ + +import { spawn } from 'node:child_process' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const DEFAULT_PROCESS_TIMEOUT_MS = 30_000 +const TSX_LOADER = fileURLToPath(import.meta.resolve('tsx')) + +/** Vitest deadline that leaves room for the subprocess-owned 30-second diagnostic timeout. */ +export const LOADER_SMOKE_TEST_TIMEOUT_MS = DEFAULT_PROCESS_TIMEOUT_MS + 15_000 + +/** Inputs that vary between real-Loader example smokes. */ +export interface LoaderSmokeOptions { + /** Human-readable example name used in failure diagnostics. */ + readonly label: string + /** Prefix for the isolated temporary process cwd. */ + readonly tempDirPrefix: string + /** Absolute stdio-agent bin path. */ + readonly binScript: string + /** Absolute real Loader config path. */ + readonly configPath: string + /** Absolute repo tsconfig path used for unbuilt workspace-package resolution. */ + readonly tsconfigPath: string + /** Environment overrides layered over the parent and isolated DSH homes. */ + readonly env?: Readonly + /** Lines written to stdin before EOF; omitted means immediate EOF. */ + readonly stdinLines?: readonly string[] + /** Process deadline override for harness tests. */ + readonly processTimeoutMs?: number +} + +/** Captured output from a Loader smoke that exited successfully. */ +export interface LoaderSmokeResult { + /** Complete stdout after clean exit. */ + readonly stdout: string + /** Complete stderr after clean exit. */ + readonly stderr: string +} + +/** + * Boot one real Loader tree from an isolated cwd, write the requested stdin + * script, close stdin, and await a clean exit. The helper owns process kill and + * temp-directory cleanup on every outcome. + * @param options - example paths, environment, stdin, and diagnostic identity. + * @returns captured stdout and stderr after a zero exit. + */ +export async function runLoaderSmoke(options: LoaderSmokeOptions): Promise { + const cwd = await mkdtemp(join(tmpdir(), options.tempDirPrefix)) + const processTimeoutMs = options.processTimeoutMs ?? DEFAULT_PROCESS_TIMEOUT_MS + try { + return await new Promise((resolve, reject) => { + const child = spawn( + process.execPath, + ['--expose-internals', '--import', TSX_LOADER, options.binScript, options.configPath], + { + cwd, + env: { + ...process.env, + DSH_HOME: join(cwd, '.dsh'), + DSH_AGENTS_HOME: join(cwd, '.agents'), + ...options.env, + TSX_TSCONFIG_PATH: options.tsconfigPath, + }, + stdio: ['pipe', 'pipe', 'pipe'], + }, + ) + let stdout = '' + let stderr = '' + let deferredFailure: Error | undefined + child.stdout.setEncoding('utf8') + child.stdout.on('data', (chunk: string) => { stdout += chunk }) + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk: string) => { stderr += chunk }) + + const timer = setTimeout(() => { + deferredFailure = new Error(`${options.label} did not exit within ${processTimeoutMs / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`) + child.kill('SIGKILL') + }, processTimeoutMs) + + child.once('exit', (code) => { + clearTimeout(timer) + if (deferredFailure !== undefined) { + reject(deferredFailure) + } else if (code === 0) { + resolve({ stdout, stderr }) + } else { + reject(new Error(`${options.label} exited ${String(code)}. stdout:\n${stdout}\nstderr:\n${stderr}`)) + } + }) + + // process.execPath and a just-created pipe make these OS-error paths + // impractical to induce without replacing the boundary under test. + /* v8 ignore start */ + child.once('error', (error) => { + clearTimeout(timer) + reject(new Error(`${options.label} failed to start: ${error.message}`)) + }) + child.stdin.once('error', (error) => { + deferredFailure ??= new Error(`${options.label} stdin failed: ${error.message}`) + child.kill('SIGKILL') + }) + /* v8 ignore stop */ + + child.stdin.end((options.stdinLines ?? []).map(line => `${line}\n`).join('')) + }) + } finally { + await rm(cwd, { recursive: true, force: true }) + } +} diff --git a/packages/support/loader-smoke/tests/fixtures/fail.ts b/packages/support/loader-smoke/tests/fixtures/fail.ts new file mode 100644 index 0000000000..98d2b44fb8 --- /dev/null +++ b/packages/support/loader-smoke/tests/fixtures/fail.ts @@ -0,0 +1,4 @@ +/** Non-zero subprocess fixture for the Loader-smoke harness. */ + +console.error('fixture failed') +process.exitCode = 7 diff --git a/packages/support/loader-smoke/tests/fixtures/hang.ts b/packages/support/loader-smoke/tests/fixtures/hang.ts new file mode 100644 index 0000000000..97b68153ff --- /dev/null +++ b/packages/support/loader-smoke/tests/fixtures/hang.ts @@ -0,0 +1,4 @@ +/** Deadline subprocess fixture for the Loader-smoke harness. */ + +console.log('fixture hanging') +setInterval(() => {}, 1_000) diff --git a/packages/support/loader-smoke/tests/fixtures/success.ts b/packages/support/loader-smoke/tests/fixtures/success.ts new file mode 100644 index 0000000000..fed57162e2 --- /dev/null +++ b/packages/support/loader-smoke/tests/fixtures/success.ts @@ -0,0 +1,16 @@ +/** Successful subprocess fixture for the Loader-smoke harness. */ + +let input = '' +process.stdin.setEncoding('utf8') +process.stdin.on('data', (chunk: string) => { input += chunk }) +process.stdin.on('end', () => { + console.log(JSON.stringify({ + configPath: process.argv[2], + cwd: process.cwd(), + dshHome: process.env.DSH_HOME, + agentsHome: process.env.DSH_AGENTS_HOME, + marker: process.env.LOADER_SMOKE_MARKER, + input, + })) + console.error('fixture stderr') +}) diff --git a/packages/support/loader-smoke/tests/loader-smoke.spec.ts b/packages/support/loader-smoke/tests/loader-smoke.spec.ts new file mode 100644 index 0000000000..4cc9f878f9 --- /dev/null +++ b/packages/support/loader-smoke/tests/loader-smoke.spec.ts @@ -0,0 +1,61 @@ +import { existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' + +const configPath = '/tmp/fixture.cordis.yml' +const tsconfigPath = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)) +const fixture = (name: string): string => fileURLToPath(new URL(`./fixtures/${name}.ts`, import.meta.url)) +const canonicalTempPath = (path: string): string => path.replace(/^\/private(?=\/var\/)/, '') + +describe('runLoaderSmoke', () => { + it('isolates the process, writes stdin, captures output, and removes the cwd', async () => { + const result = await runLoaderSmoke({ + label: 'success fixture', + tempDirPrefix: 'loader-smoke-success-', + binScript: fixture('success'), + configPath, + tsconfigPath, + env: { LOADER_SMOKE_MARKER: 'present' }, + stdinLines: ['one', 'two'], + }) + const output = JSON.parse(result.stdout) as { + configPath: string + cwd: string + dshHome: string + agentsHome: string + marker: string + input: string + } + expect(output).toMatchObject({ + configPath, + marker: 'present', + input: 'one\ntwo\n', + }) + expect(canonicalTempPath(output.dshHome)).toBe(`${canonicalTempPath(output.cwd)}/.dsh`) + expect(canonicalTempPath(output.agentsHome)).toBe(`${canonicalTempPath(output.cwd)}/.agents`) + expect(result.stderr).toContain('fixture stderr') + expect(existsSync(output.cwd)).toBe(false) + }, LOADER_SMOKE_TEST_TIMEOUT_MS) + + it('rejects a non-zero exit with captured diagnostics', async () => { + await expect(runLoaderSmoke({ + label: 'failure fixture', + tempDirPrefix: 'loader-smoke-fail-', + binScript: fixture('fail'), + configPath, + tsconfigPath, + })).rejects.toThrow('failure fixture exited 7. stdout:\n\nstderr:\nfixture failed') + }) + + it('kills a process at its deadline and reports captured output', async () => { + await expect(runLoaderSmoke({ + label: 'hanging fixture', + tempDirPrefix: 'loader-smoke-hang-', + binScript: fixture('hang'), + configPath, + tsconfigPath, + processTimeoutMs: 100, + })).rejects.toThrow('hanging fixture did not exit within 0.1s.') + }) +}) diff --git a/packages/support/loader-smoke/tsconfig.json b/packages/support/loader-smoke/tsconfig.json new file mode 100644 index 0000000000..749cb0208e --- /dev/null +++ b/packages/support/loader-smoke/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d02b4a9ca6..3498437639 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1070,6 +1070,16 @@ importers: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/support/loader-smoke: + dependencies: + tsx: + specifier: ^4.22.4 + version: 4.22.4 + devDependencies: + cordis: + specifier: ^4.0.0-rc.6 + version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/support/subagent-mock: dependencies: schemastery: diff --git a/tsconfig.build.json b/tsconfig.build.json index b3d904aa9b..9cd88de4f2 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -61,6 +61,7 @@ { "path": "./packages/ui/stdio-agent" }, { "path": "./packages/support/llm-replay" }, { "path": "./packages/support/acp-snapshot" }, + { "path": "./packages/support/loader-smoke" }, { "path": "./packages/subagent/subagent" }, { "path": "./packages/support/subagent-mock" }, { "path": "./packages/subagent/tool-subagent" }, diff --git a/tsconfig.json b/tsconfig.json index a52f21a86e..0512f823cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -72,6 +72,7 @@ { "path": "./packages/ui/stdio-agent" }, { "path": "./packages/support/llm-replay" }, { "path": "./packages/support/acp-snapshot" }, + { "path": "./packages/support/loader-smoke" }, { "path": "./packages/subagent/subagent" }, { "path": "./packages/support/subagent-mock" }, { "path": "./packages/subagent/tool-subagent" }, From 3f676efbd96dfb7954181fd59884690cb668df1c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:06:01 +0800 Subject: [PATCH 10/21] fix: bound local fetch timer config --- docs/config-catalog.md | 4 ++-- packages/web/web-fetch-local/README.md | 2 +- packages/web/web-fetch-local/src/index.ts | 14 ++++++++++++-- .../web/web-fetch-local/tests/fetch-local.spec.ts | 7 +++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 8dec62dce6..f7bb435215 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1059,7 +1059,7 @@ export interface Config { maxResponseBytes?: number /** Maximum decoded body length in characters. */ maxBodyChars?: number - /** Default fetch timeout in milliseconds. */ + /** Default fetch timeout in milliseconds, within Node's timer range. */ timeoutMs?: number /** Maximum number of same-origin redirect hops to follow. */ maxRedirects?: number @@ -1068,7 +1068,7 @@ export interface Config { } ``` -Source: [`packages/web/web-fetch-local/src/index.ts:34`](../packages/web/web-fetch-local/src/index.ts) +Source: [`packages/web/web-fetch-local/src/index.ts:36`](../packages/web/web-fetch-local/src/index.ts) ## `@deepseek-ai/dsh-web-search-deepseek` diff --git a/packages/web/web-fetch-local/README.md b/packages/web/web-fetch-local/README.md index e84ca775f0..f8120fe293 100644 --- a/packages/web/web-fetch-local/README.md +++ b/packages/web/web-fetch-local/README.md @@ -26,7 +26,7 @@ The provider's configured `timeoutMs` is a **resource backstop** for direct `ctx | `maxUrlLength` | `2048` | Maximum accepted request URL length. | | `maxResponseBytes` | `5_000_000` | Maximum response body size in bytes. | | `maxBodyChars` | `100_000` | Maximum decoded body length in characters. | -| `timeoutMs` | `30_000` | Fetch timeout — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-timeout-policy`). | +| `timeoutMs` | `30_000` | Fetch timeout within Node's timer range — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-timeout-policy`). | | `maxRedirects` | `5` | Maximum same-origin redirect hops (`0` follows none). | | `userAgent` | `deepseek-harness/…` | `User-Agent` header. | diff --git a/packages/web/web-fetch-local/src/index.ts b/packages/web/web-fetch-local/src/index.ts index 713de4c88a..471c61af2d 100644 --- a/packages/web/web-fetch-local/src/index.ts +++ b/packages/web/web-fetch-local/src/index.ts @@ -13,6 +13,8 @@ import type {} from '@deepseek-ai/dsh-web' import { LocalFetchProvider } from './provider.ts' import type { LocalFetchLimits } from './provider.ts' +const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647 + export { LOCAL_FETCH_PROVIDER_ID, LocalFetchProvider, @@ -38,7 +40,7 @@ export interface Config { maxResponseBytes?: number /** Maximum decoded body length in characters. */ maxBodyChars?: number - /** Default fetch timeout in milliseconds. */ + /** Default fetch timeout in milliseconds, within Node's timer range. */ timeoutMs?: number /** Maximum number of same-origin redirect hops to follow. */ maxRedirects?: number @@ -65,6 +67,14 @@ function assertPositiveFinite(name: string, value: number): void { } } +/** Node coerces larger timer delays to 1 ms, so reject them at configuration time. */ +function assertTimeoutMs(value: number): void { + assertPositiveFinite('timeoutMs', value) + if (value > MAX_NODE_TIMER_DELAY_MS) { + throw new Error(`web-fetch-local: timeoutMs must be no greater than ${MAX_NODE_TIMER_DELAY_MS}`) + } +} + /** The redirect hop cap must be a non-negative integer (0 follows no redirects). */ function assertNonNegativeInteger(name: string, value: number): void { if (!Number.isInteger(value) || value < 0) { @@ -79,7 +89,7 @@ export function apply(ctx: Context, config: Config): void { assertPositiveFinite('maxUrlLength', resolved.maxUrlLength) assertPositiveFinite('maxResponseBytes', resolved.maxResponseBytes) assertPositiveFinite('maxBodyChars', resolved.maxBodyChars) - assertPositiveFinite('timeoutMs', resolved.timeoutMs) + assertTimeoutMs(resolved.timeoutMs) assertNonNegativeInteger('maxRedirects', resolved.maxRedirects) const limits: LocalFetchLimits = { maxUrlLength: resolved.maxUrlLength, diff --git a/packages/web/web-fetch-local/tests/fetch-local.spec.ts b/packages/web/web-fetch-local/tests/fetch-local.spec.ts index e3eb7d30c7..3158111134 100644 --- a/packages/web/web-fetch-local/tests/fetch-local.spec.ts +++ b/packages/web/web-fetch-local/tests/fetch-local.spec.ts @@ -396,6 +396,13 @@ describe('web-fetch-local plugin registration', () => { .rejects.toThrow(/timeoutMs must be a positive finite number/) }) + it('rejects a timeout beyond Node timer range at construction', async () => { + const ctx = new Context() + await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID }) + await expect(ctx.plugin(fetchPlugin, { timeoutMs: 2_147_483_648 })) + .rejects.toThrow(/timeoutMs must be no greater than 2147483647/) + }) + it('rejects a fractional redirect cap at construction', async () => { const ctx = new Context() await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID }) From 458f87ea03432d48a0b096b2f43c8903e8db262d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:48:19 +0800 Subject: [PATCH 11/21] refactor: remove unused surface invalidation --- packages/core/session/src/surface.ts | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/core/session/src/surface.ts b/packages/core/session/src/surface.ts index 3e4ce6d89c..263322eccc 100644 --- a/packages/core/session/src/surface.ts +++ b/packages/core/session/src/surface.ts @@ -196,31 +196,18 @@ export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult export class SurfaceManager { /** Incremental state shared with the complete surface fold. */ private _state = createFoldState() - /** The last processed seq. -1 forces a full rebuild on first access. */ + /** The last processed seq. -1 forces the initial full fold. */ private _lastProcessedSeq = -1 constructor(private log: readonly SessionEvent[]) {} /** - * Reset to unprocessed state. Call after the log has been replaced - * wholesale (e.g. after Session seed). Not needed for normal appends — - * those are picked up incrementally. - */ - invalidate(): void { - this._lastProcessedSeq = -1 - // A wholesale rebuild is a rewrite: bump the generation so incremental - // consumers (the session's derived-message cache) discard their view. - this._state = createFoldState(this._state.replaceGeneration + 1) - } - - /** - * The surface's rewrite generation: bumped by every folded `replace` op and - * by {@link invalidate}. A replace is the ONE operation that rewrites the - * surface non-monotonically, so an incremental consumer of {@link nodes} - * (the session's derived-message cache) compares this between visits — an - * unchanged generation guarantees every node it has not seen is a pure tail - * append; a changed one means its view must rebuild. Monotonic: it never - * moves backwards, so comparisons cannot be fooled by a re-fold. + * The surface's rewrite generation, bumped by every folded `replace` op. A + * replace is the ONE operation that rewrites the surface non-monotonically, + * so an incremental consumer of {@link nodes} (the session's derived-message + * cache) compares this between visits — an unchanged generation guarantees + * every unseen node is a pure tail append; a changed one means its view must + * rebuild. */ get replaceGeneration(): number { if (this._lastProcessedSeq < this.log.length - 1) this._processDelta() From 4cea4979c63c2b9c4b43f01372303e0ea3dc318c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:24:21 +0800 Subject: [PATCH 12/21] docs: classify loader smoke support surface --- packages/README.md | 2 +- packages/support/loader-smoke/README.md | 10 ++++++++++ scripts/verify-package-readme-model-experience.ts | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/README.md b/packages/README.md index 9f66bbc5c1..aa3120f89b 100644 --- a/packages/README.md +++ b/packages/README.md @@ -27,7 +27,7 @@ Packages are grouped by modular role at `packages///`. The group dir | [`session-persistence/`](session-persistence/README.md) | Persistence capability family: the seam + JSONL/SQLite backends | Product — stable surface | | [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, surface records, and bounded exact reads | Product — stable surface | | [`ui/`](ui/README.md) | Editor/client integration surfaces: ACP bridge, JSON-RPC SDK server, app packages, user-approval and user-interaction seams, ask-user tool | Product — stable surface | -| [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, replay, Loader-smoke, and subagent test helpers) | Support — lower compatibility expectations | +| [`support/`](support/README.md) | Support infrastructure (invariants, replay, Loader smokes) | Support — lower compatibility expectations | | [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (the `Branded` primitive) | Support — small, stable, harness-dep-free | The split is the point: a package's group says whether it is part of the product API or support/test/example infrastructure, so release and removal decisions do not treat every package as an equal public contract. New packages join an existing group; adding a new top-level group is a deliberate act (extend the group READMEs and this table). diff --git a/packages/support/loader-smoke/README.md b/packages/support/loader-smoke/README.md index 4901924a73..ea197b25d0 100644 --- a/packages/support/loader-smoke/README.md +++ b/packages/support/loader-smoke/README.md @@ -5,3 +5,13 @@ Shared subprocess harness for keyless example smokes that boot the real stdio-ag Successful runs return stdout and stderr only after a zero exit. Non-zero exits and deadlines reject with both captured streams. `LOADER_SMOKE_TEST_TIMEOUT_MS` leaves Vitest enough room for the process-owned diagnostic timeout to fire first. This is support-tier test infrastructure, not product API. The consumers are the Loader-path smokes under `examples/{echo-agent,coding-agent,cordis-agent}`. + +## Model Experience + +None, as this test-only harness boots example processes and inspects their streams without changing an assembled model request. + +## Known Limitations and Deferred Work + +- **Only the unbuilt tsx/Loader path is exercised** — built-bin artifacts remain the responsibility of their separate e2e smokes. +- **Captured stdout and stderr are unbounded** — a runaway child can consume memory until the deadline kills it. +- **Timeout kills only the direct child** — a process tree spawned by a faulty fixture can outlive the smoke and needs external cleanup. diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index de265b23ad..2ce925cbc3 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -61,6 +61,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/subagent/subagent-subprocess': { kind: 'indirect', reason: 'Only process-based subagent backends compose a child model request.' }, 'packages/support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' }, 'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' }, + 'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' }, 'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' }, 'packages/support/subagent-mock': { kind: 'indirect', reason: 'Only dsh-tool-subagent renders its configured test outcome.' }, 'packages/ui/acp-agent': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-core and dsh-acp.' }, From 80fbeefbd6254acde77d8855e12b4e0442e59e94 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:02:11 +0800 Subject: [PATCH 13/21] refactor(ui): extract stdio plugin package Move the readline front door from stdio-agent into @deepseek-ai/dsh-stdio, keeping the loader shape and the stdio coverage with the new package. --- docs/config-catalog.md | 16 ++++++ docs/event-producer-consumer.md | 8 +-- docs/module-graph.md | 9 +++- .../2026-07-04-fold-stdio-ui-helper.md | 2 +- knip.json | 4 ++ packages/ui/README.md | 3 +- packages/ui/stdio-agent/README.md | 2 +- packages/ui/stdio-agent/package.json | 6 ++- packages/ui/stdio-agent/src/index.ts | 8 +-- .../ui/stdio-agent/tests/built-bin.e2e.ts | 1 + packages/ui/stdio-agent/tsconfig.json | 3 ++ packages/ui/stdio/README.md | 22 ++++++++ packages/ui/stdio/package.json | 42 +++++++++++++++ .../src/stdio-chat.ts => stdio/src/index.ts} | 34 +++++++++++-- packages/ui/stdio/tests/plugin-shape.spec.ts | 19 +++++++ .../tests/readline.spec.ts | 4 +- .../tests/stdio.spec.ts} | 51 ++++++++++++++++++- packages/ui/stdio/tsconfig.json | 30 +++++++++++ pnpm-lock.yaml | 28 ++++++++++ tsconfig.build.json | 1 + tsconfig.json | 1 + 21 files changed, 272 insertions(+), 22 deletions(-) create mode 100644 packages/ui/stdio/README.md create mode 100644 packages/ui/stdio/package.json rename packages/ui/{stdio-agent/src/stdio-chat.ts => stdio/src/index.ts} (92%) create mode 100644 packages/ui/stdio/tests/plugin-shape.spec.ts rename packages/ui/{stdio-agent => stdio}/tests/readline.spec.ts (93%) rename packages/ui/{stdio-agent/tests/stdio-chat.spec.ts => stdio/tests/stdio.spec.ts} (94%) create mode 100644 packages/ui/stdio/tsconfig.json diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 69fad2238e..4fa75db879 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -608,6 +608,22 @@ export interface Config { Source: [`packages/skill/skill-local/src/index.ts:39`](../packages/skill/skill-local/src/index.ts) +## `@deepseek-ai/dsh-stdio` + +Requires: `agents` · `userInteraction` + +```ts config-catalog +/** Serializable plugin configuration (cordis-native, schemastery). */ +export interface Config { + /** Banner printed once on start, before the first `> ` prompt. */ + welcome?: string + /** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */ + agent?: string +} +``` + +Source: [`packages/ui/stdio/src/index.ts:30`](../packages/ui/stdio/src/index.ts) + ## `@deepseek-ai/dsh-stdio-agent` ```ts config-catalog diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index bdf1c6e320..0da48baab6 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -7,8 +7,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio-agent`](../packages/ui/stdio-agent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) | +| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio`](../packages/ui/stdio) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio) | | `agent/error` | `emit` | [`packages/core/agent/src/types.ts:283`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | | `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:202`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`user-approval`](../packages/ui/user-approval) | | `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:212`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | @@ -16,7 +16,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:224`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | | `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:239`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) | | `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:180`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio-agent`](../packages/ui/stdio-agent) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio`](../packages/ui/stdio) | | `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:250`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | | `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:270`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | @@ -27,7 +27,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:39`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) | | `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | | `skill/provider-added` | `emit` | [`packages/skill/skill/src/index.ts:131`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - | | `skill/provider-removed` | `emit` | [`packages/skill/skill/src/index.ts:137`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - | diff --git a/docs/module-graph.md b/docs/module-graph.md index 19d471f175..950454bc1f 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -98,6 +98,7 @@ flowchart TD pkg_jsonrpc["jsonrpc"] pkg_jsonrpc_agent["jsonrpc-agent"] pkg_permission["permission"] + pkg_stdio["stdio"] pkg_stdio_agent["stdio-agent"] pkg_tool_ask_user["tool-ask-user"] pkg_user_approval["user-approval"] @@ -205,6 +206,10 @@ flowchart TD pkg_permission --> pkg_sandbox pkg_permission --> pkg_session pkg_permission --> pkg_user_approval + pkg_stdio --> pkg_agent + pkg_stdio --> pkg_llm + pkg_stdio --> pkg_session + pkg_stdio --> pkg_user_interaction pkg_agent_loop --> pkg_agent pkg_agent_loop --> pkg_llm pkg_agent_loop --> pkg_scope @@ -333,6 +338,7 @@ flowchart TD pkg_stdio_agent --> pkg_llm pkg_stdio_agent --> pkg_session pkg_stdio_agent --> pkg_session_persistence_jsonl + pkg_stdio_agent --> pkg_stdio pkg_stdio_agent --> pkg_tool_ask_user pkg_stdio_agent --> pkg_tools pkg_stdio_agent --> pkg_user_interaction @@ -385,6 +391,7 @@ flowchart TD | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | | [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) | | [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) | +| [`stdio`](../packages/ui/stdio) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-interaction`](../packages/ui/user-interaction) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | @@ -410,4 +417,4 @@ flowchart TD | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`acp-agent`](../packages/ui/acp-agent) | `ui` | [`acp`](../packages/ui/acp), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | -| [`stdio-agent`](../packages/ui/stdio-agent) | `ui` | [`agent`](../packages/core/agent), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | +| [`stdio-agent`](../packages/ui/stdio-agent) | `ui` | [`agent`](../packages/core/agent), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | diff --git a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md index 2c7626619b..634e8ac6ca 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md +++ b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md @@ -10,7 +10,7 @@ The boundary bought package metadata, workspace and tsconfig references, module- ## Decision -The `stdio-chat` module now lives inside `dsh-stdio-agent` with its runtime seam. Per-file tests cover EOF, rendering, disposal, and piped-versus-TTY behavior without replacing process globals. It retains the named Cordis plugin export shape consumed by the app; an `unwrapExports` assertion and keyless Loader smokes guard both the package and composed entry paths. +The helper lives in `@deepseek-ai/dsh-stdio` as the terminal-channel plugin (`packages/ui/stdio/src/index.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio/tests/stdio.spec.ts`, `readline.spec.ts`) moved with it, so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered under the per-file coverage gate without hijacking process globals. The module keeps the named `name`/`inject`/`Config`/`apply` export shape — the contract the app's `ctx.plugin(uiStdio, …)` mount consumes — and the keyless Loader-path smokes in `examples/echo-agent` and `examples/coding-agent` keep proving the composed tree boots through the real Loader (the stdio package's plugin-shape unit suite pins the explicit `unwrapExports` assertion, since a bundle without `inject` would boot past a stray default rather than crash). The `packages/support/ui-stdio` package is gone: manifest, tsconfig references, module-graph rows, and README rows deleted; the doc comments that named the package (the example e2e module docs, `packages/README.md`, the support and todo READMEs, [the ui group README](../../../../packages/ui/README.md)) describe the in-package module. diff --git a/knip.json b/knip.json index 5b8fc7f436..52956ef21a 100644 --- a/knip.json +++ b/knip.json @@ -85,6 +85,10 @@ "entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"], "project": ["src/**/*.ts", "tests/**/*.ts"] }, + "packages/ui/stdio": { + "entry": ["tests/**/*.spec.ts"], + "project": ["src/**/*.ts", "tests/**/*.ts"] + }, "packages/ui/jsonrpc-agent": { "project": ["src/**/*.ts"] }, diff --git a/packages/ui/README.md b/packages/ui/README.md index 02dfcfbdce..29ff591ee5 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -9,13 +9,14 @@ Integrations that expose the agent to an external editor or client. These are ** | `permission/` | User-facing permission presets (`workspace-write`/`danger-full-access`): one product-level select bundling the sandbox-mode and approval-policy knobs, written through to their session events | `ctx.permission` | | `user-interaction/` | Abstract human question/answer seam used by UI-backed confirmation tools | `ctx.userInteraction` | | `tool-ask-user/` | Model-facing `ask_user_question` tool over `ctx.userInteraction` | (registers on `ctx.tools`) | +| `stdio/` | Terminal readline channel over `ctx.agents`, `session/event`, and `ctx.userInteraction`; agent lifecycle stays with app/developer code | (drives `ctx.agents`) | | `stdio-agent/` | Terminal stdio chat APP: the agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) | | `acp-agent/` | ACP server APP: the agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) | | `jsonrpc/` | Stdio JSON-RPC server for out-of-process SDK clients | (drives `ctx.agents`) | | `jsonrpc-agent/` | Bin-only SDK runtime app that boots an external `cordis.yml` | (`bin` only) | | `app-boot/` | Shared boot glue for the app bins: `.env` loading, fail-loud Loader guards, snapshot-aware config resolution, the settle-the-tree boot sequence | (library for the bins) | -A UI integration is a client-driver plugin, not a loop change or capability seam: it consumes the existing `agent/*` events and `dsh-agent` factory. `jsonrpc` is the SDK-client sibling of the `acp` editor bridge. The readline UI lives inside [`stdio-agent/`](stdio-agent/README.md) because it is scaffolding for that front door, not an independently swappable integration. +A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The `jsonrpc` plugin is the SDK-client sibling of the `acp` bridge (a JSON-RPC server over `ctx.agents` for out-of-process SDK clients rather than editors). The [`stdio`](stdio/README.md) plugin is the unstructured readline analogue of the `acp` bridge; app bundles and SDK projects compose it explicitly with the services and tools their product profile selects. `user-approval`, `user-interaction`, and `tool-ask-user` live here because asking a human is a UI-backed product affordance, not part of the providerless core spine. `user-approval` owns the one-shot `ctx.approval` decision mechanism and its policy tier; answerers remain with their UI channel owners. `user-interaction` remains provider-neutral (`ctx.userInteraction`), while `tool-ask-user` is its model-facing consumer and the app/bridge packages provide concrete providers. diff --git a/packages/ui/stdio-agent/README.md b/packages/ui/stdio-agent/README.md index 8cd97f5cc7..5a594d0701 100644 --- a/packages/ui/stdio-agent/README.md +++ b/packages/ui/stdio-agent/README.md @@ -15,7 +15,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha | `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` | | `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools | | `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool | -| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent | +| `@deepseek-ai/dsh-stdio` | the readline UI, bound to the `main` agent | `@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, NOT baked in here: it is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader`, and the in-process test tier cannot even import it (so a package whose `apply` statically pulled it in could never carry the per-file coverage gate). Unlike the console logger, a stray `hmr` is not a stdout-purity footgun, so leaving it at the leaf costs no safety. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`. diff --git a/packages/ui/stdio-agent/package.json b/packages/ui/stdio-agent/package.json index fcd432a9fd..73a25f8c57 100644 --- a/packages/ui/stdio-agent/package.json +++ b/packages/ui/stdio-agent/package.json @@ -38,9 +38,10 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-agent-core": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", + "@deepseek-ai/dsh-stdio": "^0.0.1", "@deepseek-ai/dsh-tool-ask-user": "^0.0.1", + "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", "cordis": "^4.0.0-rc.6", "schemastery": "^3.17.0" @@ -55,9 +56,10 @@ "@deepseek-ai/dsh-agent-core": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-stdio": "workspace:^", "@deepseek-ai/dsh-tool-ask-user": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", "cordis": "^4.0.0-rc.6", "schemastery": "^3.17.0" diff --git a/packages/ui/stdio-agent/src/index.ts b/packages/ui/stdio-agent/src/index.ts index cd8167d658..fb125cec56 100644 --- a/packages/ui/stdio-agent/src/index.ts +++ b/packages/ui/stdio-agent/src/index.ts @@ -1,8 +1,8 @@ /** * The stdio chat app: the default agent spine ({@link @deepseek-ai/dsh-agent-core}) plus the - * coupled front-door cluster a terminal chat needs — a console logger, the readline UI (the - * in-package `stdio-chat` module), JSONL session persistence, and a pre-created `main` agent - * the UI drives. + * coupled front-door cluster a terminal chat needs — a console logger, the independently + * packaged readline UI, JSONL session persistence, the user-interaction seam with its + * `ask_user_question` tool, and a pre-created `main` agent the UI drives. * Swappable adapters, executors, optional tools, and HMR stay in the leaf. This * Loader plugin intentionally exposes named exports only; a default export * would hide its `Config` schema (see docs/postmortem/0001). @@ -19,7 +19,7 @@ import * as agentCore from '@deepseek-ai/dsh-agent-core' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user' -import * as uiStdio from './stdio-chat.ts' +import * as uiStdio from '@deepseek-ai/dsh-stdio' export const name = 'stdio-agent' diff --git a/packages/ui/stdio-agent/tests/built-bin.e2e.ts b/packages/ui/stdio-agent/tests/built-bin.e2e.ts index ddd7ca454e..1702e31772 100644 --- a/packages/ui/stdio-agent/tests/built-bin.e2e.ts +++ b/packages/ui/stdio-agent/tests/built-bin.e2e.ts @@ -24,6 +24,7 @@ const dshPackages = [ 'bash/tool-bash', 'support/invariants', 'ui/app-boot', 'session-persistence/session-persistence', 'session-persistence/session-persistence-jsonl', 'ui/stdio-agent', + 'ui/stdio', 'ui/tool-ask-user', 'ui/user-interaction', ] const vendorPackages = [ 'cordis', 'loader', 'include', 'timer', 'hmr', 'logger-console', diff --git a/packages/ui/stdio-agent/tsconfig.json b/packages/ui/stdio-agent/tsconfig.json index 6f30c1558e..b0bfa760c3 100644 --- a/packages/ui/stdio-agent/tsconfig.json +++ b/packages/ui/stdio-agent/tsconfig.json @@ -35,6 +35,9 @@ { "path": "../user-interaction" }, + { + "path": "../stdio" + }, { "path": "../tool-ask-user" }, diff --git a/packages/ui/stdio/README.md b/packages/ui/stdio/README.md new file mode 100644 index 0000000000..2bd4995d86 --- /dev/null +++ b/packages/ui/stdio/README.md @@ -0,0 +1,22 @@ +# @deepseek-ai/dsh-stdio + +The terminal readline front door for DeepSeek Harness agents. It reads prompts from stdin, sends or steers them through `ctx.agents`, renders the durable `session/event` transcript to stdout, and answers `ctx.userInteraction` requests in the same terminal. + +This package owns the terminal channel only. It injects `agents` and `userInteraction`, then drives an agent created or resumed by app or developer code. The agent spine, agent lifecycle, console logger, and model-facing [`ask_user_question`](../tool-ask-user/README.md) tool remain separate composition entries. + +## Config + +| Key | Default | Meaning | +|---|---|---| +| `welcome` | `ready.` | Banner printed before the first prompt | +| `agent` | `main` | Agent id driven by stdin and observed for EOF shutdown | + +The plugin seeds display labels from the live agent registry, then tracks `agent/created` and `agent/disposed` so HMR and externally managed agents render consistently. Disposal closes readline and unregisters every listener/provider through Cordis effects. + +```yaml +- id: stdio + name: '@deepseek-ai/dsh-stdio' + config: + welcome: 'agent REPL ready. Give it a coding task.' + agent: main +``` diff --git a/packages/ui/stdio/package.json b/packages/ui/stdio/package.json new file mode 100644 index 0000000000..3b00dc6625 --- /dev/null +++ b/packages/ui/stdio/package.json @@ -0,0 +1,42 @@ +{ + "name": "@deepseek-ai/dsh-stdio", + "description": "Terminal readline front door for driving and rendering DeepSeek Harness agents over stdio", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-llm": "^0.0.1", + "@deepseek-ai/dsh-session": "^0.0.1", + "@deepseek-ai/dsh-user-interaction": "^0.0.1", + "cordis": "^4.0.0-rc.6" + }, + "dependencies": { + "schemastery": "^3.18.0" + }, + "devDependencies": { + "@cordisjs/plugin-loader": "workspace:^", + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-user-interaction": "workspace:^", + "cordis": "^4.0.0-rc.6" + } +} diff --git a/packages/ui/stdio-agent/src/stdio-chat.ts b/packages/ui/stdio/src/index.ts similarity index 92% rename from packages/ui/stdio-agent/src/stdio-chat.ts rename to packages/ui/stdio/src/index.ts index 60aba12026..1e665381ce 100644 --- a/packages/ui/stdio-agent/src/stdio-chat.ts +++ b/packages/ui/stdio/src/index.ts @@ -2,7 +2,11 @@ * The stdio app's readline UI: reads lines from stdin into `agent.send()` or * `steer()`, renders the durable event stream to stdout, and exits piped input * only after submitted work reaches idle. - * @module @deepseek-ai/dsh-stdio-agent/stdio-chat + * + * This package is the independently composable stdio front door. It establishes + * the terminal channel and drives an agent created or resumed by app or + * developer code. + * @module @deepseek-ai/dsh-stdio */ import { createInterface } from 'node:readline' @@ -26,8 +30,6 @@ export const inject = ['agents', 'userInteraction'] export interface Config { /** Banner printed once on start, before the first `> ` prompt. */ welcome?: string - // TODO(fixed-stdio-agent): this app-internal plugin is mounted only for the - // precreated `main` agent; remove configurability and its config-only test. /** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */ agent?: string } @@ -350,16 +352,38 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt }, 'ui-stdio') } +/** + * Open the terminal channel once its configured agent exists. Generated stdio + * projects boot the Cordis tree first and create or resume the agent from + * developer code immediately afterward, so stdin must remain untouched until + * the matching `agent/created` notification arrives. + * @param ctx - the context supplying the agent registry and event stream. + * @param config - presentation and target-agent configuration. + * @param runtime - process-I/O seam. + */ +export function mountStdio(ctx: Context, config: Config, runtime: StdioRuntime): void { + const agentId = AgentId(config.agent ?? 'main') + if (ctx.agents.get(agentId) !== undefined) { + createStdioChat(ctx, config, runtime) + return + } + const dispose = ctx.on('agent/created', (agent) => { + if (agent.id !== agentId) return + dispose() + createStdioChat(ctx, config, runtime) + }) +} + /** * Cordis entry point. Binds the real `process` streams and delegates to - * {@link createStdioChat}; the indirection keeps the side-effecting handles out + * {@link mountStdio}; the indirection keeps the side-effecting handles out * of the testable core, which is why the unit suite drives `createStdioChat` * directly. This thin wrapper is exercised end-to-end by the keyless * Loader-path e2e smoke in `examples/echo-agent` (the real product entry). */ /* v8 ignore start -- production stdio wiring; testable core is createStdioChat() (covered), exercised e2e by echo-agent keyless smoke */ export function apply(ctx: Context, config: Config): void { - createStdioChat(ctx, config, { + mountStdio(ctx, config, { input: process.stdin, output: process.stdout, exit: code => process.exit(code), diff --git a/packages/ui/stdio/tests/plugin-shape.spec.ts b/packages/ui/stdio/tests/plugin-shape.spec.ts new file mode 100644 index 0000000000..5b2b35f65e --- /dev/null +++ b/packages/ui/stdio/tests/plugin-shape.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import Loader from '@cordisjs/plugin-loader' +import * as stdio from '../src/index.ts' + +/** Real Loader export-path guard for the namespace stdio plugin. */ +describe('dsh-stdio plugin export shape', () => { + it('preserves name, inject, Config, and apply through Loader unwrapping', () => { + expect('default' in stdio).toBe(false) + expect(typeof stdio.apply).toBe('function') + + const loader = Object.create(Loader.prototype) as Loader + const unwrapped = loader.unwrapExports(stdio) as Record + expect(unwrapped).toBe(stdio) + expect(unwrapped.name).toBe('ui-stdio') + expect(unwrapped.inject).toEqual(['agents', 'userInteraction']) + expect(unwrapped.Config).toBeDefined() + expect(typeof unwrapped.apply).toBe('function') + }) +}) diff --git a/packages/ui/stdio-agent/tests/readline.spec.ts b/packages/ui/stdio/tests/readline.spec.ts similarity index 93% rename from packages/ui/stdio-agent/tests/readline.spec.ts rename to packages/ui/stdio/tests/readline.spec.ts index a958c435c1..638e98bf59 100644 --- a/packages/ui/stdio-agent/tests/readline.spec.ts +++ b/packages/ui/stdio/tests/readline.spec.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events' import type { Readable, Writable } from 'node:stream' import { describe, expect, it, vi } from 'vitest' import type { Context } from 'cordis' -import type { StdioRuntime } from '../src/stdio-chat.ts' +import type { StdioRuntime } from '../src/index.ts' const createInterface = vi.hoisted(() => vi.fn(() => { const reader = new EventEmitter() as EventEmitter & { close(): void } @@ -33,7 +33,7 @@ function fakeRuntime(inputIsTTY: boolean, outputIsTTY: boolean): StdioRuntime { describe('createStdioChat readline mode', () => { it('enables terminal editing only when both stdio streams are TTYs', async () => { - const { createStdioChat } = await import('../src/stdio-chat.ts') + const { createStdioChat } = await import('../src/index.ts') const tty = fakeRuntime(true, true) createStdioChat(fakeContext(), {}, tty) diff --git a/packages/ui/stdio-agent/tests/stdio-chat.spec.ts b/packages/ui/stdio/tests/stdio.spec.ts similarity index 94% rename from packages/ui/stdio-agent/tests/stdio-chat.spec.ts rename to packages/ui/stdio/tests/stdio.spec.ts index f734acfd0d..7bb6a6f245 100644 --- a/packages/ui/stdio-agent/tests/stdio-chat.spec.ts +++ b/packages/ui/stdio/tests/stdio.spec.ts @@ -6,7 +6,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent' import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' -import { createStdioChat, type Config, type StdioRuntime } from '../src/stdio-chat.ts' +import { createStdioChat, mountStdio, type Config, type StdioRuntime } from '../src/index.ts' /** * Unit tests for the stdio UI plugin. They drive the REAL plugin body @@ -93,6 +93,55 @@ function flushExit(): Promise { return new Promise(resolve => setTimeout(resolve, 250)) } +describe('mountStdio readiness', () => { + it('leaves stdin untouched until the configured agent is created', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + const { runtime, out } = makeRuntime() + const fiber = await ctx.plugin(Object.assign((inner: Context) => { + mountStdio(inner, CONFIG, runtime) + }, { inject: ['agents', 'userInteraction'] })) + + expect(out.text()).toBe('') + ctx.agents.register(makeAgent('other')) + expect(out.text()).toBe('') + ctx.agents.register(makeAgent('main')) + expect(out.text()).toBe('hi there\n> ') + await fiber.dispose() + }) + + it('opens immediately when the configured agent already exists', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + ctx.agents.register(makeAgent('main')) + const { runtime, out } = makeRuntime() + const fiber = await ctx.plugin(Object.assign((inner: Context) => { + mountStdio(inner, CONFIG, runtime) + }, { inject: ['agents', 'userInteraction'] })) + + expect(out.text()).toBe('hi there\n> ') + await fiber.dispose() + }) + + it('waits for main when no target agent is configured', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserInteractionService) + const { runtime, out } = makeRuntime() + const fiber = await ctx.plugin(Object.assign((inner: Context) => { + mountStdio(inner, { welcome: 'ready' }, runtime) + }, { inject: ['agents', 'userInteraction'] })) + + ctx.agents.register(makeAgent('other')) + expect(out.text()).toBe('') + ctx.agents.register(makeAgent('main')) + expect(out.text()).toBe('ready\n> ') + await fiber.dispose() + }) +}) + describe('createStdioChat rendering', () => { it('writes the welcome banner and prompt on start', async () => { const { out } = await setup() diff --git a/packages/ui/stdio/tsconfig.json b/packages/ui/stdio/tsconfig.json new file mode 100644 index 0000000000..00cb815a75 --- /dev/null +++ b/packages/ui/stdio/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../core/session" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../user-interaction" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1905b80887..eee88947bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,6 +172,9 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-stdio': + specifier: workspace:^ + version: link:../stdio '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt @@ -1392,6 +1395,31 @@ importers: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/ui/stdio: + dependencies: + schemastery: + specifier: ^3.18.0 + version: 3.18.0 + devDependencies: + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-user-interaction': + specifier: workspace:^ + version: link:../user-interaction + cordis: + specifier: ^4.0.0-rc.6 + version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) + packages/ui/stdio-agent: devDependencies: '@cordisjs/plugin-include': diff --git a/tsconfig.build.json b/tsconfig.build.json index 591955b260..3bd15b81ea 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -61,6 +61,7 @@ { "path": "./packages/ui/app-boot" }, { "path": "./packages/ui/jsonrpc" }, { "path": "./packages/ui/jsonrpc-agent" }, + { "path": "./packages/ui/stdio" }, { "path": "./packages/ui/stdio-agent" }, { "path": "./packages/support/llm-replay" }, { "path": "./packages/support/acp-snapshot" }, diff --git a/tsconfig.json b/tsconfig.json index e97a8295a5..a243abaa1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -72,6 +72,7 @@ { "path": "./packages/ui/app-boot" }, { "path": "./packages/ui/jsonrpc" }, { "path": "./packages/ui/jsonrpc-agent" }, + { "path": "./packages/ui/stdio" }, { "path": "./packages/ui/stdio-agent" }, { "path": "./packages/support/llm-replay" }, { "path": "./packages/support/acp-snapshot" }, From c685582d541e0be874b40f833605fb3cb0b1934e Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:39:18 +0800 Subject: [PATCH 14/21] vendor: update cordis / loader --- vendor/README.md | 7 +++---- vendor/cordis/package.json | 4 ++-- vendor/cordis/src/events.ts | 6 +++--- vendor/cordis/src/fiber.ts | 4 ++-- vendor/cordis/src/reflect.ts | 5 +++++ vendor/loader/package.json | 10 ++++++++-- vendor/loader/src/internal.ts | 24 +++++++++++++++++------- 7 files changed, 40 insertions(+), 20 deletions(-) diff --git a/vendor/README.md b/vendor/README.md index e27385f836..9bf226fb4d 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -14,15 +14,15 @@ Upstream workspace: `cordis-workspace` (local checkout: `~/repos/cordis-workspac |---|---|---|---|---| | `cosmokit/` | `cosmokit` | 1.8.1 | https://github.com/deepseek-harness/cosmokit | `16f6fc058ade66e8ac5da0033d35a8d0f279f544` | | `schemastery/` | `schemastery` | 3.18.0 | https://github.com/deepseek-harness/schemastery (`packages/core`) | `e67cee00ad725bd1534aee930a979ea3eec6f698` | -| `cordis/` | `cordis` | 4.0.0-rc.6 | https://github.com/deepseek-harness/cordis (`packages/core`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | -| `loader/` | `@cordisjs/plugin-loader` | 1.0.0-rc.4 | https://github.com/deepseek-harness/cordis (`packages/loader`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | +| `cordis/` | `cordis` | 4.0.0-rc.7 | https://github.com/cordiverse/cordis (`packages/core`) | `56b3d4f725681cf4556c1a8695a709cc3b6eed74` | +| `loader/` | `@cordisjs/plugin-loader` | 1.0.0-rc.5 | https://github.com/cordiverse/cordis (`packages/loader`) | `56b3d4f725681cf4556c1a8695a709cc3b6eed74` | | `include/` | `@cordisjs/plugin-include` | 1.0.4 | https://github.com/deepseek-harness/cordis (`packages/include`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | | `group/` | `@cordisjs/plugin-group` | 1.0.0 | https://github.com/deepseek-harness/cordis (`packages/group`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | | `timer/` | `@cordisjs/plugin-timer` | 1.1.2 | https://github.com/deepseek-harness/cordis (`packages/timer`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | | `hmr/` | `@cordisjs/plugin-hmr` | 1.0.15 | https://github.com/deepseek-harness/cordis (`packages/hmr`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | | `logger-console/` | `@cordisjs/plugin-logger-console` | 1.0.0 | https://github.com/deepseek-harness/cordis (`packages/logger-console`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | -Third-party dependencies of the vendored packages stay on npm: `@standard-schema/spec`, `js-yaml`, `chokidar`, `picomatch`, `@babel/code-frame`, `supports-color`. +Third-party dependencies of the vendored packages stay on npm: `@standard-schema/spec`, `js-yaml`, `chokidar`, `picomatch`, `@babel/code-frame`, `supports-color`, `node-addon-require-builtin`. Intentionally **not** vendored (verified unused by this set): `reggol`, `@cordisjs/utils`, `@cordisjs/element`, `@cordisjs/unyaml` (dev-time YAML import hook only). @@ -36,7 +36,6 @@ Keep this log exhaustive — every divergence from upstream must be listed. 4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`. 5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface. 6. **`cordis/src/fiber.ts` lifecycle hardening**: locally closes three reentrant disposal gaps. An effect's owner-list wrapper is registered before its setup body runs, so an unload begun from inside setup awaits setup and every collected cleanup; synchronous setup failure removes the wrapper and rolls back collected cleanup. Async cleanup stays owner-visible until quiescence, and Cordis's internal effect composition joins an already-running cleanup while repeated public disposer calls retain their upstream single-shot result. Effect creation is rejected while the owner is `UNLOADING` (while `PENDING` and `LOADING` remain legal), preventing cleanup-time registrations from escaping the unload snapshot. Child fibers register and receive their parent-owned disposer before `internal/plugin` publication, resolve dependency declarations added by that notification before activation, drain effects attached while pending, skip plugin execution when reentrant disposal invalidates the load epoch before its first checkpoint, and contain teardown-notification failures per observer so one callback cannot starve peers or interrupt ownership cleanup. -7. **`cordis/src/events.ts`**: a `FIXME` documents the upstream `parallel()` bug where a synchronous listener throw aborts callback enumeration and starves later listeners; runtime behavior remains upstream-identical pending an upstream fix. ## Sync procedure diff --git a/vendor/cordis/package.json b/vendor/cordis/package.json index 9d9ac07a34..80a327c2dd 100644 --- a/vendor/cordis/package.json +++ b/vendor/cordis/package.json @@ -1,7 +1,7 @@ { "name": "cordis", "description": "Meta-Framework for Modern JavaScript Applications", - "version": "4.0.0-rc.6", + "version": "4.0.0-rc.7", "private": true, "sideEffects": false, "type": "module", @@ -26,7 +26,7 @@ "license": "MIT", "peerDependencies": { "@cordisjs/plugin-include": "^1.0.4", - "@cordisjs/plugin-loader": "^1.0.0-rc.4" + "@cordisjs/plugin-loader": "^1.0.0-rc.5" }, "peerDependenciesMeta": { "@cordisjs/plugin-include": { diff --git a/vendor/cordis/src/events.ts b/vendor/cordis/src/events.ts index e483afadf5..842a780cdb 100644 --- a/vendor/cordis/src/events.ts +++ b/vendor/cordis/src/events.ts @@ -106,9 +106,9 @@ export class EventsService { /** Run listeners concurrently and wait for all of them. */ async parallel(...args: any[]) { - // FIXME(cordis upstream): A synchronous listener throw aborts callback - // enumeration here and starves later parallel listeners. Fix upstream. - await Promise.all(this.dispatch('emit', args).map(cb => cb(...args))) + const results = await Promise.allSettled(this.dispatch('emit', args).map(async cb => cb(...args))) + const errors = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected') + if (errors.length) throw new AggregateError(errors.map(error => error.reason)) } /** Run listeners synchronously without waiting for returned promises. */ diff --git a/vendor/cordis/src/fiber.ts b/vendor/cordis/src/fiber.ts index 7e7766b48d..43a320142f 100644 --- a/vendor/cordis/src/fiber.ts +++ b/vendor/cordis/src/fiber.ts @@ -189,7 +189,7 @@ export class Fiber { this._runner = { epoch: INACTIVE, getOuterStack, - execute: () => { + execute: function () { if (isConstructor(runtime.callback)) { // eslint-disable-next-line new-cap const instance = new runtime.callback(this.ctx, this.config) @@ -307,7 +307,7 @@ export class Fiber { throw new TypeError('Invalid effect') } } - const effect: Effect = runner.execute() + const effect: Effect = runner.execute.call(this) if (typeof effect === 'function') { return runner.collect(effect) } else if (isNullable(effect)) { diff --git a/vendor/cordis/src/reflect.ts b/vendor/cordis/src/reflect.ts index 212ec4e779..c084067af1 100644 --- a/vendor/cordis/src/reflect.ts +++ b/vendor/cordis/src/reflect.ts @@ -229,6 +229,11 @@ export class ReflectService { fibers.push(fiber) } } + for (const name of names) { + const self: Context = Object.create(this.ctx) + self[symbols.filter] = (target: Context) => filter(target, name) + this.ctx.events.emit(self, 'internal/service', name, this._getImpl(name, false)?.value) + } return fibers } diff --git a/vendor/loader/package.json b/vendor/loader/package.json index fde6d01d27..ad5f14f7cd 100644 --- a/vendor/loader/package.json +++ b/vendor/loader/package.json @@ -1,7 +1,7 @@ { "name": "@cordisjs/plugin-loader", "description": "Plugin loader for cordis", - "version": "1.0.0-rc.4", + "version": "1.0.0-rc.5", "private": true, "type": "module", "main": "lib/index.js", @@ -23,7 +23,13 @@ "author": "Shigma ", "license": "MIT", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7", + "node-addon-require-builtin": "^0.1.0" + }, + "peerDependenciesMeta": { + "node-addon-require-builtin": { + "optional": true + } }, "dependencies": { "cosmokit": "^1.8.1" diff --git a/vendor/loader/src/internal.ts b/vendor/loader/src/internal.ts index 6e1e2c6780..083e45475f 100644 --- a/vendor/loader/src/internal.ts +++ b/vendor/loader/src/internal.ts @@ -105,18 +105,28 @@ export type ModuleLoader = ModuleLoaderV1 | ModuleLoaderV2 export namespace ModuleLoader { let _cachedLoader: ModuleLoader | undefined - export function fromInternal(): ModuleLoader | undefined { - if (!process.execArgv.includes('--expose-internals')) return - if (_cachedLoader) return _cachedLoader + function requireInternal(id: string): any { const require = createRequire(import.meta.url) + if (process.execArgv.includes('--expose-internals')) { + try { + return require(id) + } catch {} + } + try { + return require('node-addon-require-builtin').requireBuiltin(id) + } catch {} + } + + export function fromInternal(): ModuleLoader | undefined { + if (_cachedLoader) return _cachedLoader const [major] = process.versions.node.split('.').map(Number) if (major >= 24) { - const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader() - return _cachedLoader = Object.assign(raw, { version: 'v2' }) + const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader() + if (raw) return _cachedLoader = Object.assign(raw, { version: 'v2' }) } else if (major >= 22) { - const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader() - return _cachedLoader = Object.assign(raw, { version: 'v1' }) + const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader() + if (raw) return _cachedLoader = Object.assign(raw, { version: 'v1' }) } } } From 8e892095a16682ecb1a3fc491ce96d0eaaaf5fdb Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:20:48 +0800 Subject: [PATCH 15/21] fix: readme and dep --- examples/acp-agent/cordis.snapshot.yml | 10 ++++++++++ packages/ui/stdio/README.md | 20 ++++++++++++++++++++ pnpm-lock.yaml | 6 +++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/examples/acp-agent/cordis.snapshot.yml b/examples/acp-agent/cordis.snapshot.yml index f69770b4dd..90a2fa6a2a 100644 --- a/examples/acp-agent/cordis.snapshot.yml +++ b/examples/acp-agent/cordis.snapshot.yml @@ -15,6 +15,16 @@ - id: llm-deepseek name: '@deepseek-ai/dsh-llm-deepseek' disabled: true + - id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' + config: + runnerCommand: + - bash + - -c + - while [ "$1" != "--" ]; do shift; done; shift; exec "$@" + - passthrough-runner + runnerFailureSignatures: + - 'passthrough-runner: profile rejected' - insert: - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' diff --git a/packages/ui/stdio/README.md b/packages/ui/stdio/README.md index 2bd4995d86..b7d320880d 100644 --- a/packages/ui/stdio/README.md +++ b/packages/ui/stdio/README.md @@ -20,3 +20,23 @@ The plugin seeds display labels from the live agent registry, then tracks `agent welcome: 'agent REPL ready. Give it a coding task.' agent: main ``` + +## Model Experience + +### Readline prompt input + +**What the model sees**: Each non-empty terminal line outside an active question becomes one text block, sent with `agent.send()` while the target agent is idle and `agent.steer()` while it is running. + +**Token effect**: Submitted text is retained under the agent loop's normal session-history and compaction rules. The welcome banner, `> ` prompt, rendered transcript, and `[tool call]` / `[tool result]` terminal lines add no tokens. + +### Terminal user-interaction answers + +**What the model sees**: When a consumer calls `ctx.userInteraction.ask()`, this provider renders the question in the terminal and returns selected option labels or `custom` text. Through `dsh-tool-ask-user`, closed stdin becomes `Error: ask_user_question cannot be answered because stdin is closed`; disposal or abort becomes `Error: ask_user_question was interrupted before the user answered`. + +**Token effect**: Waiting and terminal prompts add no tokens; the resolved answer or error is model-visible only through the calling tool or plugin's result. + +## Known Limitations and Deferred Work + +- **One configured agent receives stdin** — the session/event renderer can print output from any session, but input lines always drive the configured `agent` id rather than routing by the visible label. +- **Terminal questions are text-only and sequential** — the provider queues asks, supports option labels plus custom text, and has no richer UI shapes such as file pickers or diff previews. +- **Closed stdin ends the terminal channel** — EOF rejects active or queued questions and exits after submitted work reaches idle; there is no reconnect path for a long-lived process. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eee88947bc..3cab8bad46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,9 +172,6 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session - '@deepseek-ai/dsh-stdio': - specifier: workspace:^ - version: link:../stdio '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt @@ -1449,6 +1446,9 @@ importers: '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session-persistence/session-persistence-jsonl + '@deepseek-ai/dsh-stdio': + specifier: workspace:^ + version: link:../stdio '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt From 31f4441fd43797548e4810b218dfc8385b65d2cc Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:28:45 +0800 Subject: [PATCH 16/21] pkg: update vendor dep --- packages/bash/bash-local/package.json | 4 +- packages/bash/bash-sandbox/package.json | 4 +- packages/bash/bash/package.json | 4 +- packages/bash/tool-bash/package.json | 4 +- .../code-runtime-worker/package.json | 4 +- .../code-runtime/code-runtime/package.json | 4 +- packages/compact/compact-basic/package.json | 4 +- packages/compact/compact/package.json | 4 +- packages/context/time-context/package.json | 4 +- packages/cordis/tool-cordis/package.json | 6 +- packages/core/agent-core/package.json | 4 +- packages/core/agent-loop/package.json | 4 +- packages/core/agent/package.json | 4 +- packages/core/scope/package.json | 4 +- packages/core/session/package.json | 4 +- packages/core/system-prompt/package.json | 4 +- packages/core/tools/package.json | 4 +- packages/fs/fs-local/package.json | 4 +- packages/fs/fs-policy/package.json | 4 +- packages/fs/fs/package.json | 4 +- packages/fs/tool-fs/package.json | 4 +- packages/guard/repeat-tool-guard/package.json | 4 +- packages/hooks/hook-protocol/package.json | 4 +- packages/hooks/hooks-claude/package.json | 4 +- packages/hooks/hooks-codex/package.json | 4 +- packages/llm/llm-deepseek/package.json | 4 +- packages/llm/llm-pi-ai/package.json | 4 +- packages/llm/llm/package.json | 4 +- packages/sandbox/sandbox-local/package.json | 4 +- .../sandbox-local/tests/packed-install.e2e.ts | 2 +- packages/sandbox/sandbox/package.json | 4 +- .../session-persistence-jsonl/package.json | 4 +- .../session-persistence-sqlite/package.json | 4 +- .../session-persistence/package.json | 4 +- .../session-query/session-query/package.json | 4 +- packages/skill/skill-local/package.json | 4 +- packages/skill/skill/package.json | 4 +- packages/skill/tool-skill/package.json | 4 +- packages/subagent/subagent-acp/package.json | 6 +- packages/subagent/subagent-fork/package.json | 6 +- .../subagent/subagent-inprocess/package.json | 4 +- packages/subagent/subagent-spawn/package.json | 6 +- .../subagent/subagent-subprocess/package.json | 4 +- packages/subagent/subagent/package.json | 4 +- packages/subagent/tool-subagent/package.json | 6 +- packages/support/acp-snapshot/package.json | 4 +- packages/support/invariants/package.json | 4 +- packages/support/llm-replay/package.json | 4 +- packages/support/subagent-mock/package.json | 6 +- packages/timeout/timeout-policy/package.json | 4 +- packages/todo/tool-todo/package.json | 4 +- packages/ui/acp-agent/package.json | 6 +- packages/ui/acp/package.json | 4 +- packages/ui/app-boot/package.json | 6 +- packages/ui/jsonrpc-agent/package.json | 4 +- packages/ui/jsonrpc/package.json | 4 +- packages/ui/permission/package.json | 4 +- packages/ui/stdio-agent/package.json | 6 +- packages/ui/tool-ask-user/package.json | 4 +- packages/ui/user-approval/package.json | 4 +- packages/ui/user-interaction/package.json | 4 +- packages/util/brand/package.json | 4 +- packages/util/timeout/package.json | 4 +- packages/web/tool-web/package.json | 4 +- packages/web/web-fetch-local/package.json | 4 +- packages/web/web-search-deepseek/package.json | 4 +- packages/web/web-search-exa/package.json | 4 +- .../web/web-search-perplexity/package.json | 4 +- packages/web/web/package.json | 4 +- packages/workflow/tool-workflow/package.json | 4 +- .../workflow-workerthread/package.json | 4 +- packages/workflow/workflow/package.json | 4 +- pnpm-lock.yaml | 500 +++++++++++------- pnpm-workspace.yaml | 5 + vendor/group/package.json | 4 +- vendor/hmr/package.json | 2 +- vendor/include/package.json | 4 +- vendor/logger-console/package.json | 2 +- vendor/timer/package.json | 2 +- 79 files changed, 469 insertions(+), 354 deletions(-) diff --git a/packages/bash/bash-local/package.json b/packages/bash/bash-local/package.json index e3c7ffe33b..381855b465 100644 --- a/packages/bash/bash-local/package.json +++ b/packages/bash/bash-local/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-timeout": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -32,6 +32,6 @@ "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/bash/bash-sandbox/package.json b/packages/bash/bash-sandbox/package.json index 0077511946..b4f61abcbe 100644 --- a/packages/bash/bash-sandbox/package.json +++ b/packages/bash/bash-sandbox/package.json @@ -25,7 +25,7 @@ "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-bash-local": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -36,6 +36,6 @@ "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-sandbox-local": "workspace:^", "node-addon-landlock-run": "0.0.0-test.0", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/bash/bash/package.json b/packages/bash/bash/package.json index bfa71d73e3..2ae0566142 100644 --- a/packages/bash/bash/package.json +++ b/packages/bash/bash/package.json @@ -25,12 +25,12 @@ "@deepseek-ai/dsh-brand": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/bash/tool-bash/package.json b/packages/bash/tool-bash/package.json index eec5d79ccb..4e23a928a7 100644 --- a/packages/bash/tool-bash/package.json +++ b/packages/bash/tool-bash/package.json @@ -29,7 +29,7 @@ "@deepseek-ai/dsh-sandbox": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -44,6 +44,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/code-runtime/code-runtime-worker/package.json b/packages/code-runtime/code-runtime-worker/package.json index 91075243d2..c9d25ef4d8 100644 --- a/packages/code-runtime/code-runtime-worker/package.json +++ b/packages/code-runtime/code-runtime-worker/package.json @@ -28,13 +28,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-code-runtime": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { "@deepseek-ai/dsh-code-runtime": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/code-runtime/code-runtime/package.json b/packages/code-runtime/code-runtime/package.json index 0fe24bb15c..5380d26ace 100644 --- a/packages/code-runtime/code-runtime/package.json +++ b/packages/code-runtime/code-runtime/package.json @@ -22,9 +22,9 @@ ], "license": "BSD-3-Clause", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/compact/compact-basic/package.json b/packages/compact/compact-basic/package.json index e57e28a8c9..32852a060f 100644 --- a/packages/compact/compact-basic/package.json +++ b/packages/compact/compact-basic/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-compact": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -37,6 +37,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/compact/compact/package.json b/packages/compact/compact/package.json index 99efd25b9c..985c42d3b2 100644 --- a/packages/compact/compact/package.json +++ b/packages/compact/compact/package.json @@ -24,11 +24,11 @@ "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index e18bb32540..f319c7a5b1 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -27,7 +27,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -36,6 +36,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/cordis/tool-cordis/package.json b/packages/cordis/tool-cordis/package.json index a7d99eeea3..fd9c35e48e 100644 --- a/packages/cordis/tool-cordis/package.json +++ b/packages/cordis/tool-cordis/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -37,8 +37,8 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6", + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7", "@cordisjs/plugin-timer": "workspace:^" } } diff --git a/packages/core/agent-core/package.json b/packages/core/agent-core/package.json index 039a6ac505..2c2b25e772 100644 --- a/packages/core/agent-core/package.json +++ b/packages/core/agent-core/package.json @@ -34,7 +34,7 @@ "@deepseek-ai/dsh-tool-bash": "^0.0.1", "@deepseek-ai/dsh-tool-skill": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@cordisjs/plugin-timer": "workspace:^", @@ -49,7 +49,7 @@ "@deepseek-ai/dsh-tool-bash": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 5d180bd08c..7e2fb235a2 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -28,7 +28,7 @@ "@deepseek-ai/dsh-session-persistence": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -43,6 +43,6 @@ "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index b8e6108904..72cd18942e 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -27,7 +27,7 @@ "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", @@ -35,6 +35,6 @@ "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json index 89c2b4428b..88d78ceb8b 100644 --- a/packages/core/scope/package.json +++ b/packages/core/scope/package.json @@ -22,9 +22,9 @@ ], "license": "BSD-3-Clause", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/core/session/package.json b/packages/core/session/package.json index 454a0d7cc3..540c5cdc75 100644 --- a/packages/core/session/package.json +++ b/packages/core/session/package.json @@ -25,12 +25,12 @@ "@deepseek-ai/dsh-brand": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index 120e10ef11..67161b79b4 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -32,6 +32,6 @@ "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index 9b4b80d67c..2fe3cbd448 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -29,7 +29,7 @@ "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -42,6 +42,6 @@ "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index 4945684713..dd80cb4d9c 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -23,7 +23,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -31,6 +31,6 @@ "devDependencies": { "@deepseek-ai/dsh-fs": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/fs/fs-policy/package.json b/packages/fs/fs-policy/package.json index c3f2a07982..e27302e4a6 100644 --- a/packages/fs/fs-policy/package.json +++ b/packages/fs/fs-policy/package.json @@ -23,11 +23,11 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-fs": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index 813cb04e16..f1efde152a 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -24,11 +24,11 @@ "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index 7e7b78aa38..c21c806e98 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -31,7 +31,7 @@ "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -44,6 +44,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/guard/repeat-tool-guard/package.json b/packages/guard/repeat-tool-guard/package.json index 9b085bb015..0cc99b6976 100644 --- a/packages/guard/repeat-tool-guard/package.json +++ b/packages/guard/repeat-tool-guard/package.json @@ -27,7 +27,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -36,6 +36,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/hooks/hook-protocol/package.json b/packages/hooks/hook-protocol/package.json index 2220220769..201c744219 100644 --- a/packages/hooks/hook-protocol/package.json +++ b/packages/hooks/hook-protocol/package.json @@ -24,11 +24,11 @@ "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/hooks/hooks-claude/package.json b/packages/hooks/hooks-claude/package.json index 5cc39f9999..21f08965d8 100644 --- a/packages/hooks/hooks-claude/package.json +++ b/packages/hooks/hooks-claude/package.json @@ -31,7 +31,7 @@ "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -44,6 +44,6 @@ "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json index f26b57fe11..fe667b0302 100644 --- a/packages/hooks/hooks-codex/package.json +++ b/packages/hooks/hooks-codex/package.json @@ -30,7 +30,7 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -42,6 +42,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index 8ebf71c5b5..1461ad0f44 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -23,13 +23,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index 30911915ff..c922467deb 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -23,7 +23,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "@earendil-works/pi-ai": "^0.79.1", @@ -32,6 +32,6 @@ "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index 1dc84e13d7..ab11c8574f 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -23,10 +23,10 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index d2f8b34161..9dd90a3e9a 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "node-addon-landlock-run": "0.0.0-test.0", @@ -33,6 +33,6 @@ "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts index 62cb56dc31..8519357c1b 100644 --- a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts @@ -71,7 +71,7 @@ describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish- // Peer ranges resolve to the tarballs; Cordis is pinned to their peer range. Do not omit optional // dependencies because the launcher selects its OS/CPU package through one. writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ name: 'dsh-packed-consumer', private: true, type: 'module' })) - const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs, 'cordis@4.0.0-rc.6'], { + const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs, 'cordis@4.0.0-rc.7'], { cwd: consumerDir, encoding: 'utf8', timeout: 300_000, diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json index b37ef714ea..50c5b443ba 100644 --- a/packages/sandbox/sandbox/package.json +++ b/packages/sandbox/sandbox/package.json @@ -23,10 +23,10 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/session-persistence/session-persistence-jsonl/package.json b/packages/session-persistence/session-persistence-jsonl/package.json index ac18a38838..ddb9f2af4d 100644 --- a/packages/session-persistence/session-persistence-jsonl/package.json +++ b/packages/session-persistence/session-persistence-jsonl/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -32,6 +32,6 @@ "devDependencies": { "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/session-persistence/session-persistence-sqlite/package.json b/packages/session-persistence/session-persistence-sqlite/package.json index b26c69461e..f367b737b3 100644 --- a/packages/session-persistence/session-persistence-sqlite/package.json +++ b/packages/session-persistence/session-persistence-sqlite/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -32,6 +32,6 @@ "devDependencies": { "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/session-persistence/session-persistence/package.json b/packages/session-persistence/session-persistence/package.json index ed6c80dfd9..91eef09007 100644 --- a/packages/session-persistence/session-persistence/package.json +++ b/packages/session-persistence/session-persistence/package.json @@ -23,10 +23,10 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-session": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-session": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index 9f78d4f1db..e87327de13 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -25,7 +25,7 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "peerDependenciesMeta": { "@deepseek-ai/dsh-session-persistence": { @@ -39,6 +39,6 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/skill/skill-local/package.json b/packages/skill/skill-local/package.json index dcacc5960a..d1ca775a26 100644 --- a/packages/skill/skill-local/package.json +++ b/packages/skill/skill-local/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", "@deepseek-ai/dsh-skill": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0", @@ -33,6 +33,6 @@ "devDependencies": { "@deepseek-ai/dsh-fs": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/skill/skill/package.json b/packages/skill/skill/package.json index b303e16bed..c025de6ee9 100644 --- a/packages/skill/skill/package.json +++ b/packages/skill/skill/package.json @@ -22,12 +22,12 @@ ], "license": "BSD-3-Clause", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/skill/tool-skill/package.json b/packages/skill/tool-skill/package.json index bf7a77a49e..3d6ddc6b7e 100644 --- a/packages/skill/tool-skill/package.json +++ b/packages/skill/tool-skill/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-skill": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -38,6 +38,6 @@ "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-skill-local": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-acp/package.json b/packages/subagent/subagent-acp/package.json index e73d861a79..5093e3df40 100644 --- a/packages/subagent/subagent-acp/package.json +++ b/packages/subagent/subagent-acp/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-subagent-subprocess": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "@agentclientprotocol/sdk": "0.25.1", @@ -37,7 +37,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subagent-subprocess": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-fork/package.json b/packages/subagent/subagent-fork/package.json index 7b1c40c4f3..8794884518 100644 --- a/packages/subagent/subagent-fork/package.json +++ b/packages/subagent/subagent-fork/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-subagent-inprocess": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -42,7 +42,7 @@ "@deepseek-ai/dsh-subagent-spawn": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-inprocess/package.json b/packages/subagent/subagent-inprocess/package.json index 4e6b72533a..aa80dcac3e 100644 --- a/packages/subagent/subagent-inprocess/package.json +++ b/packages/subagent/subagent-inprocess/package.json @@ -28,7 +28,7 @@ "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -39,6 +39,6 @@ "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-spawn/package.json b/packages/subagent/subagent-spawn/package.json index 087371ded2..f2500a4a56 100644 --- a/packages/subagent/subagent-spawn/package.json +++ b/packages/subagent/subagent-spawn/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-subagent-inprocess": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -43,7 +43,7 @@ "@deepseek-ai/dsh-tool-bash": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-subprocess/package.json b/packages/subagent/subagent-subprocess/package.json index 68f525dd8e..5f17459276 100644 --- a/packages/subagent/subagent-subprocess/package.json +++ b/packages/subagent/subagent-subprocess/package.json @@ -22,9 +22,9 @@ ], "license": "BSD-3-Clause", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index eb0dbf8da0..0b09033fd3 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -26,13 +26,13 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index 254c9e2928..88d4f8e4f3 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -38,7 +38,7 @@ "@deepseek-ai/dsh-subagent-mock": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/support/acp-snapshot/package.json b/packages/support/acp-snapshot/package.json index 363bc86e25..d206b08161 100644 --- a/packages/support/acp-snapshot/package.json +++ b/packages/support/acp-snapshot/package.json @@ -27,9 +27,9 @@ "vitest": "^4.1.8" }, "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/support/invariants/package.json b/packages/support/invariants/package.json index 85a249dbd1..59a425387b 100644 --- a/packages/support/invariants/package.json +++ b/packages/support/invariants/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -37,6 +37,6 @@ "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/support/llm-replay/package.json b/packages/support/llm-replay/package.json index ce57ea18ef..403f3bda92 100644 --- a/packages/support/llm-replay/package.json +++ b/packages/support/llm-replay/package.json @@ -24,11 +24,11 @@ "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/support/subagent-mock/package.json b/packages/support/subagent-mock/package.json index 8980cc35d2..a4ed0a0c6e 100644 --- a/packages/support/subagent-mock/package.json +++ b/packages/support/subagent-mock/package.json @@ -25,7 +25,7 @@ "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -34,7 +34,7 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/timeout/timeout-policy/package.json b/packages/timeout/timeout-policy/package.json index 9069735b86..aa351cb7a6 100644 --- a/packages/timeout/timeout-policy/package.json +++ b/packages/timeout/timeout-policy/package.json @@ -25,12 +25,12 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-timeout": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index f2d4344f99..9ff69d7c76 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -25,7 +25,7 @@ "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -34,6 +34,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/acp-agent/package.json b/packages/ui/acp-agent/package.json index 987ddb6c13..0c1a45e713 100644 --- a/packages/ui/acp-agent/package.json +++ b/packages/ui/acp-agent/package.json @@ -31,14 +31,14 @@ "license": "BSD-3-Clause", "peerDependencies": { "@cordisjs/plugin-include": "^1.0.4", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", + "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@deepseek-ai/dsh-app-boot": "^0.0.1", "@deepseek-ai/dsh-acp": "^0.0.1", "@deepseek-ai/dsh-agent-core": "^0.0.1", "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", - "cordis": "^4.0.0-rc.6", + "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" }, "devDependencies": { @@ -52,7 +52,7 @@ "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", - "cordis": "^4.0.0-rc.6", + "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" } } diff --git a/packages/ui/acp/package.json b/packages/ui/acp/package.json index b0b038760b..71efc51a07 100644 --- a/packages/ui/acp/package.json +++ b/packages/ui/acp/package.json @@ -37,7 +37,7 @@ "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-user-approval": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -61,6 +61,6 @@ "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/app-boot/package.json b/packages/ui/app-boot/package.json index 67b8b00dbc..1eef56ae93 100644 --- a/packages/ui/app-boot/package.json +++ b/packages/ui/app-boot/package.json @@ -23,12 +23,12 @@ "license": "BSD-3-Clause", "peerDependencies": { "@cordisjs/plugin-include": "^1.0.4", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@cordisjs/plugin-include": "workspace:^", "@cordisjs/plugin-loader": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/jsonrpc-agent/package.json b/packages/ui/jsonrpc-agent/package.json index bef09ad15f..1919a7336a 100644 --- a/packages/ui/jsonrpc-agent/package.json +++ b/packages/ui/jsonrpc-agent/package.json @@ -33,9 +33,9 @@ "@deepseek-ai/dsh-app-boot": "workspace:^" }, "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/jsonrpc/package.json b/packages/ui/jsonrpc/package.json index 94355fe570..be98f173de 100644 --- a/packages/ui/jsonrpc/package.json +++ b/packages/ui/jsonrpc/package.json @@ -30,7 +30,7 @@ "@deepseek-ai/dsh-llm-deepseek": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@cordisjs/plugin-loader": "workspace:^", @@ -41,6 +41,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/permission/package.json b/packages/ui/permission/package.json index b6833791e4..e38e5a7bf1 100644 --- a/packages/ui/permission/package.json +++ b/packages/ui/permission/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-sandbox": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-user-approval": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -36,6 +36,6 @@ "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/stdio-agent/package.json b/packages/ui/stdio-agent/package.json index fcd432a9fd..b182e25df8 100644 --- a/packages/ui/stdio-agent/package.json +++ b/packages/ui/stdio-agent/package.json @@ -31,7 +31,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@cordisjs/plugin-include": "^1.0.4", - "@cordisjs/plugin-loader": "^1.0.0-rc.4", + "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@deepseek-ai/dsh-app-boot": "^0.0.1", "@cordisjs/plugin-logger-console": "^1.0.0", "@deepseek-ai/dsh-agent": "^0.0.1", @@ -42,7 +42,7 @@ "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", "@deepseek-ai/dsh-tool-ask-user": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", - "cordis": "^4.0.0-rc.6", + "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" }, "devDependencies": { @@ -59,7 +59,7 @@ "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-tool-ask-user": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", - "cordis": "^4.0.0-rc.6", + "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" } } diff --git a/packages/ui/tool-ask-user/package.json b/packages/ui/tool-ask-user/package.json index c1860f48f0..5ee90f0818 100644 --- a/packages/ui/tool-ask-user/package.json +++ b/packages/ui/tool-ask-user/package.json @@ -25,7 +25,7 @@ "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -33,6 +33,6 @@ "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/user-approval/package.json b/packages/ui/user-approval/package.json index 602fee53af..1696a7b603 100644 --- a/packages/ui/user-approval/package.json +++ b/packages/ui/user-approval/package.json @@ -28,7 +28,7 @@ "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -40,6 +40,6 @@ "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/user-interaction/package.json b/packages/ui/user-interaction/package.json index f333195ac7..f4c9c411fd 100644 --- a/packages/ui/user-interaction/package.json +++ b/packages/ui/user-interaction/package.json @@ -24,11 +24,11 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/util/brand/package.json b/packages/util/brand/package.json index 8059952170..7074aaa621 100644 --- a/packages/util/brand/package.json +++ b/packages/util/brand/package.json @@ -22,9 +22,9 @@ ], "license": "BSD-3-Clause", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/util/timeout/package.json b/packages/util/timeout/package.json index 150a155324..381b9d269e 100644 --- a/packages/util/timeout/package.json +++ b/packages/util/timeout/package.json @@ -22,9 +22,9 @@ ], "license": "BSD-3-Clause", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index 80fd69dbc3..f6b5791f0f 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -26,7 +26,7 @@ "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-web": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -41,6 +41,6 @@ "@deepseek-ai/dsh-web": "workspace:^", "@deepseek-ai/dsh-web-fetch-local": "workspace:^", "@deepseek-ai/dsh-web-search-exa": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/web/web-fetch-local/package.json b/packages/web/web-fetch-local/package.json index 9b847db6f3..1e1d7ea71b 100644 --- a/packages/web/web-fetch-local/package.json +++ b/packages/web/web-fetch-local/package.json @@ -24,7 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-timeout": "^0.0.1", "@deepseek-ai/dsh-web": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -32,6 +32,6 @@ "devDependencies": { "@deepseek-ai/dsh-timeout": "workspace:^", "@deepseek-ai/dsh-web": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json index 617e9f2768..42471006c0 100644 --- a/packages/web/web-search-deepseek/package.json +++ b/packages/web/web-search-deepseek/package.json @@ -23,13 +23,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-web": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { "@deepseek-ai/dsh-web": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/web/web-search-exa/package.json b/packages/web/web-search-exa/package.json index a111daa287..240909e24a 100644 --- a/packages/web/web-search-exa/package.json +++ b/packages/web/web-search-exa/package.json @@ -23,13 +23,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-web": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { "@deepseek-ai/dsh-web": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/web/web-search-perplexity/package.json b/packages/web/web-search-perplexity/package.json index fde44ddd16..26d077fada 100644 --- a/packages/web/web-search-perplexity/package.json +++ b/packages/web/web-search-perplexity/package.json @@ -23,13 +23,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-web": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { "@deepseek-ai/dsh-web": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/web/web/package.json b/packages/web/web/package.json index 8c68c58203..b94f7ba685 100644 --- a/packages/web/web/package.json +++ b/packages/web/web/package.json @@ -23,13 +23,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-llm": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { "@deepseek-ai/dsh-llm": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/workflow/tool-workflow/package.json b/packages/workflow/tool-workflow/package.json index f5c2138d85..327e6ec3a9 100644 --- a/packages/workflow/tool-workflow/package.json +++ b/packages/workflow/tool-workflow/package.json @@ -27,7 +27,7 @@ "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-workflow": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -41,6 +41,6 @@ "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-workflow": "workspace:^", "@deepseek-ai/dsh-workflow-workerthread": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/workflow/workflow-workerthread/package.json b/packages/workflow/workflow-workerthread/package.json index afaf840f78..485e252c97 100644 --- a/packages/workflow/workflow-workerthread/package.json +++ b/packages/workflow/workflow-workerthread/package.json @@ -34,7 +34,7 @@ "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-workflow": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" @@ -51,7 +51,7 @@ "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-workflow": "workspace:^", - "cordis": "^4.0.0-rc.6", + "cordis": "^4.0.0-rc.7", "tsx": "^4.19.2" } } diff --git a/packages/workflow/workflow/package.json b/packages/workflow/workflow/package.json index a6c004d6d0..696955db4f 100644 --- a/packages/workflow/workflow/package.json +++ b/packages/workflow/workflow/package.json @@ -25,13 +25,13 @@ "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-brand": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1905b80887..b40b8e69db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,8 +99,8 @@ importers: specifier: workspace:^ version: link:../../core/session cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/bash/bash-local: dependencies: @@ -115,8 +115,8 @@ importers: specifier: workspace:^ version: link:../../util/timeout cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/bash/bash-sandbox: dependencies: @@ -137,8 +137,8 @@ importers: specifier: workspace:^ version: link:../../sandbox/sandbox-local cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) node-addon-landlock-run: specifier: 0.0.0-test.0 version: 0.0.0-test.0 @@ -182,14 +182,14 @@ importers: specifier: workspace:^ version: link:../../ui/user-approval cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/code-runtime/code-runtime: devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/code-runtime/code-runtime-worker: dependencies: @@ -201,8 +201,8 @@ importers: specifier: workspace:^ version: link:../code-runtime cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/compact/compact: devDependencies: @@ -213,8 +213,8 @@ importers: specifier: workspace:^ version: link:../../core/session cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/compact/compact-basic: devDependencies: @@ -243,8 +243,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/context/time-context: dependencies: @@ -271,8 +271,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/cordis/tool-cordis: dependencies: @@ -281,8 +281,8 @@ importers: version: 3.18.0 devDependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) '@cordisjs/plugin-timer': specifier: workspace:^ version: link:../../../vendor/timer @@ -308,8 +308,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/core/agent: devDependencies: @@ -329,8 +329,8 @@ importers: specifier: workspace:^ version: link:../system-prompt cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/core/agent-core: dependencies: @@ -375,8 +375,8 @@ importers: specifier: workspace:^ version: link:../tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/core/agent-loop: dependencies: @@ -412,14 +412,14 @@ importers: specifier: workspace:^ version: link:../tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/core/scope: devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/core/session: devDependencies: @@ -433,8 +433,8 @@ importers: specifier: workspace:^ version: link:../scope cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/core/system-prompt: dependencies: @@ -449,8 +449,8 @@ importers: specifier: workspace:^ version: link:../scope cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/core/tools: dependencies: @@ -480,8 +480,8 @@ importers: specifier: workspace:^ version: link:../../ui/user-approval cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/fs/fs: devDependencies: @@ -492,8 +492,8 @@ importers: specifier: workspace:^ version: link:../../llm/llm cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/fs/fs-local: dependencies: @@ -508,8 +508,8 @@ importers: specifier: workspace:^ version: link:../../llm/llm cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/fs/fs-policy: devDependencies: @@ -520,8 +520,8 @@ importers: specifier: workspace:^ version: link:../../llm/llm cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/fs/tool-fs: dependencies: @@ -563,8 +563,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/guard/repeat-tool-guard: dependencies: @@ -591,8 +591,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/hooks/hook-protocol: devDependencies: @@ -603,8 +603,8 @@ importers: specifier: workspace:^ version: link:../../core/session cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/hooks/hooks-claude: dependencies: @@ -643,8 +643,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/hooks/hooks-codex: dependencies: @@ -680,8 +680,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/llm/llm: devDependencies: @@ -689,8 +689,8 @@ importers: specifier: workspace:^ version: link:../../util/brand cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/llm/llm-deepseek: dependencies: @@ -702,8 +702,8 @@ importers: specifier: workspace:^ version: link:../llm cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/llm/llm-pi-ai: dependencies: @@ -721,8 +721,8 @@ importers: specifier: workspace:^ version: link:../llm-deepseek cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/sandbox/sandbox: devDependencies: @@ -730,8 +730,8 @@ importers: specifier: workspace:^ version: link:../../llm/llm cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/sandbox/sandbox-local: dependencies: @@ -749,8 +749,8 @@ importers: specifier: workspace:^ version: link:../sandbox cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/session-persistence/session-persistence: devDependencies: @@ -758,8 +758,8 @@ importers: specifier: workspace:^ version: link:../../core/session cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/session-persistence/session-persistence-jsonl: dependencies: @@ -774,8 +774,8 @@ importers: specifier: workspace:^ version: link:../session-persistence cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/session-persistence/session-persistence-sqlite: dependencies: @@ -790,8 +790,8 @@ importers: specifier: workspace:^ version: link:../session-persistence cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/session-query/session-query: dependencies: @@ -809,8 +809,8 @@ importers: specifier: workspace:^ version: link:../../session-persistence/session-persistence cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/skill/skill: dependencies: @@ -819,8 +819,8 @@ importers: version: 3.18.0 devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/skill/skill-local: dependencies: @@ -838,8 +838,8 @@ importers: specifier: workspace:^ version: link:../skill cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/skill/tool-skill: dependencies: @@ -866,8 +866,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/subagent/subagent: devDependencies: @@ -884,8 +884,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/subagent/subagent-acp: dependencies: @@ -897,8 +897,8 @@ importers: version: 3.18.0 devDependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent @@ -912,8 +912,8 @@ importers: specifier: workspace:^ version: link:../subagent-subprocess cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/subagent/subagent-fork: dependencies: @@ -922,8 +922,8 @@ importers: version: 3.18.0 devDependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent @@ -955,8 +955,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/subagent/subagent-inprocess: devDependencies: @@ -985,8 +985,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/subagent/subagent-spawn: dependencies: @@ -995,8 +995,8 @@ importers: version: 3.18.0 devDependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent @@ -1037,14 +1037,14 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/subagent/subagent-subprocess: devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/subagent/tool-subagent: dependencies: @@ -1053,8 +1053,8 @@ importers: version: 3.18.0 devDependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent @@ -1074,8 +1074,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/support/acp-snapshot: dependencies: @@ -1090,8 +1090,8 @@ importers: version: 4.1.8(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/support/invariants: devDependencies: @@ -1120,8 +1120,8 @@ importers: specifier: workspace:^ version: link:../../ui/user-approval cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/support/llm-replay: devDependencies: @@ -1132,8 +1132,8 @@ importers: specifier: workspace:^ version: link:../../core/session cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/support/subagent-mock: dependencies: @@ -1142,8 +1142,8 @@ importers: version: 3.18.0 devDependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent @@ -1154,8 +1154,8 @@ importers: specifier: workspace:^ version: link:../../subagent/subagent cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/timeout/timeout-policy: devDependencies: @@ -1169,8 +1169,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/todo/tool-todo: devDependencies: @@ -1193,8 +1193,8 @@ importers: specifier: workspace:^ version: link:../../core/tools cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/ui/acp: dependencies: @@ -1272,8 +1272,8 @@ importers: specifier: workspace:^ version: link:../user-interaction cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/ui/acp-agent: devDependencies: @@ -1308,8 +1308,8 @@ importers: specifier: workspace:^ version: link:../user-interaction cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) schemastery: specifier: ^3.17.0 version: 3.18.0 @@ -1323,8 +1323,8 @@ importers: specifier: workspace:^ version: link:../../../vendor/loader cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) packages/ui/jsonrpc: dependencies: @@ -1357,8 +1357,8 @@ importers: specifier: workspace:^ version: link:../../subagent/subagent cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) packages/ui/jsonrpc-agent: dependencies: @@ -1367,8 +1367,8 @@ importers: version: link:../app-boot devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/ui/permission: dependencies: @@ -1389,8 +1389,8 @@ importers: specifier: workspace:^ version: link:../user-approval cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/ui/stdio-agent: devDependencies: @@ -1434,8 +1434,8 @@ importers: specifier: workspace:^ version: link:../user-interaction cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) schemastery: specifier: ^3.17.0 version: 3.18.0 @@ -1458,8 +1458,8 @@ importers: specifier: workspace:^ version: link:../user-interaction cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/ui/user-approval: dependencies: @@ -1486,8 +1486,8 @@ importers: specifier: workspace:^ version: link:../../core/system-prompt cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/ui/user-interaction: devDependencies: @@ -1498,20 +1498,20 @@ importers: specifier: workspace:^ version: link:../../llm/llm cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/util/brand: devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/util/timeout: devDependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/web/tool-web: dependencies: @@ -1547,8 +1547,8 @@ importers: specifier: workspace:^ version: link:../web-search-exa cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/web/web: dependencies: @@ -1560,8 +1560,8 @@ importers: specifier: workspace:^ version: link:../../llm/llm cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/web/web-fetch-local: dependencies: @@ -1576,8 +1576,8 @@ importers: specifier: workspace:^ version: link:../web cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/web/web-search-deepseek: dependencies: @@ -1589,8 +1589,8 @@ importers: specifier: workspace:^ version: link:../web cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/web/web-search-exa: dependencies: @@ -1602,8 +1602,8 @@ importers: specifier: workspace:^ version: link:../web cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/web/web-search-perplexity: dependencies: @@ -1615,8 +1615,8 @@ importers: specifier: workspace:^ version: link:../web cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/workflow/tool-workflow: dependencies: @@ -1649,8 +1649,8 @@ importers: specifier: workspace:^ version: link:../workflow-workerthread cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/workflow/workflow: devDependencies: @@ -1667,8 +1667,8 @@ importers: specifier: workspace:^ version: link:../../core/session cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/workflow/workflow-workerthread: dependencies: @@ -1710,8 +1710,8 @@ importers: specifier: workspace:^ version: link:../workflow cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) tsx: specifier: ^4.19.2 version: 4.22.4 @@ -1921,10 +1921,10 @@ importers: dependencies: '@cordisjs/plugin-include': specifier: ^1.0.4 - version: 1.0.4(@cordisjs/plugin-loader@1.0.0-rc.4)(cordis@4.0.0-rc.6) + version: 1.0.4(@cordisjs/plugin-loader@1.0.0-rc.5)(cordis@4.0.0-rc.7) '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) '@standard-schema/spec': specifier: ^1.1.0 version: 1.1.0 @@ -1937,11 +1937,11 @@ importers: vendor/group: dependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) vendor/hmr: dependencies: @@ -1950,13 +1950,13 @@ importers: version: 7.29.7 '@cordisjs/plugin-timer': specifier: ^1.1.2 - version: 1.1.2(cordis@4.0.0-rc.6) + version: 1.1.2(cordis@4.0.0-rc.7) chokidar: specifier: ^4.0.3 version: 4.0.3 cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: specifier: ^1.8.1 version: 1.8.1 @@ -1980,11 +1980,11 @@ importers: vendor/include: dependencies: '@cordisjs/plugin-loader': - specifier: ^1.0.0-rc.4 - version: 1.0.0-rc.4(cordis@4.0.0-rc.6) + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: specifier: ^1.8.1 version: 1.8.1 @@ -1995,17 +1995,20 @@ importers: vendor/loader: dependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: specifier: ^1.8.1 version: 1.8.1 + node-addon-require-builtin: + specifier: ^0.1.0 + version: 0.1.0 vendor/logger-console: dependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: specifier: ^1.8.1 version: 1.8.1 @@ -2028,8 +2031,8 @@ importers: vendor/timer: dependencies: cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: specifier: ^1.8.1 version: 1.8.1 @@ -2235,10 +2238,14 @@ packages: '@cordisjs/plugin-loader': ^1.0.0-rc.4 cordis: ^4.0.0-rc.5 - '@cordisjs/plugin-loader@1.0.0-rc.4': - resolution: {integrity: sha512-pocUsZiZ/r2yOJby79tmn22Ifk3tCpOmHNYar4TPAotSja30soSrnMVU8YIRD/vJdjuDCMnM/46nDQWDFLM3SQ==} + '@cordisjs/plugin-loader@1.0.0-rc.5': + resolution: {integrity: sha512-084Wn2SzkFinbaASTq8blHOUqQt/oxZfX6gnrt0lnJ1CrystulFLL1+XVgF4o7lUMN9bHn4cfT1pMtkHprCtHw==} peerDependencies: - cordis: ^4.0.0-rc.5 + cordis: ^4.0.0-rc.7 + node-addon-require-builtin: ^0.1.0 + peerDependenciesMeta: + node-addon-require-builtin: + optional: true '@cordisjs/plugin-timer@1.1.2': resolution: {integrity: sha512-5z5C3Eewt8JzK9XGy5JgIoYFRqXPWZnT7hHFfuJMQNzSom6iEVeLXpYiMvqVqGfJicHA7IroaOjcLRf99sidrQ==} @@ -3431,12 +3438,12 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cordis@4.0.0-rc.6: - resolution: {integrity: sha512-GzUv7zCKh3FlgM3/Ad2S03UpYO3v4u1GcKa7ig4K2je4lCrgJ/S64ziiZI6XNyKEa1tZwdzj4oBQrhYDLgfEiA==} + cordis@4.0.0-rc.7: + resolution: {integrity: sha512-5nm6ehrSfJhEUV659CctEvyNuBY/AXapw8+ZEw7YENztdzpiT+Ha8nIfkyhfyAgPtJns9aB5On5nzl9Sm6zHeQ==} hasBin: true peerDependencies: '@cordisjs/plugin-include': ^1.0.4 - '@cordisjs/plugin-loader': ^1.0.0-rc.4 + '@cordisjs/plugin-loader': ^1.0.0-rc.5 peerDependenciesMeta: '@cordisjs/plugin-include': optional: true @@ -4397,6 +4404,58 @@ packages: resolution: {integrity: sha512-c5qopltRonjW6+VinXYMp4FVi9Sxf8eQEb/9G82EksQQ+JC4CDprv+ko5URWmtyZ3wD4GFdeRTyw1AG5yCwJhQ==} engines: {node: '>=20'} + node-addon-native-custom-loader@0.1.0: + resolution: {integrity: sha512-LtkRZWBshiGdWB9K7yuQuEeQaoYfWSFMwV52wi4kKsQSRCSjB4Sf4lEgQTiOlVmOznM0Bg9EABLKBowkCDucQQ==} + engines: {node: '>=20'} + + node-addon-require-builtin-darwin-arm64@0.1.0: + resolution: {integrity: sha512-KXmOO2gs5um5HXt8k9sZMnNwrgTdnRKOf5NMXLyrY6pc3QCJrdCY6J6STgurjoM4GXOJlfo2emEfOxrIS0sCbg==} + engines: {node: '>=20'} + cpu: [arm64] + os: [darwin] + + node-addon-require-builtin-darwin-x64@0.1.0: + resolution: {integrity: sha512-m1JkvBslC4ooNUvlvQoOx96d0qk2M1e+bO2gVkl+TT0SD07VGAPXNkxZvyT+O3A63i/1j0rjJ88B78sSdyDiVA==} + engines: {node: '>=20'} + cpu: [x64] + os: [darwin] + + node-addon-require-builtin-linux-arm64-gnu@0.1.0: + resolution: {integrity: sha512-76fYWMzYBeT6eunBUrxAUleyMZwQfp8FgB3XLDCrQAsDtH0UVdg+zgmbVIQeJyJQdxTnfsQ9sfvdymG96ZeZew==} + engines: {node: '>=20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + node-addon-require-builtin-linux-x64-gnu@0.1.0: + resolution: {integrity: sha512-dDOumCPgJheVfcHOVq2nCQUp3mRU0Qsu6MfnZzVZMA73tSeQwA+yoUuQW3oPz/wuE51LwXEkm6Se9aerawi0Ng==} + engines: {node: '>=20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + node-addon-require-builtin-win32-arm64-msvc@0.1.0: + resolution: {integrity: sha512-OJ7m8r074Wbtc8mLsh+ugIP4KCwsTyDzfB7FE+7eeSSYRgQlbSOC11jMOYIWqMalLhAWCLkRBw7fYJDty3sSAw==} + engines: {node: '>=20'} + cpu: [arm64] + os: [win32] + + node-addon-require-builtin-win32-ia32-msvc@0.1.0: + resolution: {integrity: sha512-qUhC7MEP0NhuNMwlnPudYIBtPKlUo9McRi3PWsv4539hFar1RfxGmU4DZYtDQk07Bms/NAlIE8X7mxKVu8E+OQ==} + engines: {node: '>=20 <23'} + cpu: [ia32] + os: [win32] + + node-addon-require-builtin-win32-x64-msvc@0.1.0: + resolution: {integrity: sha512-JHiuwzW6jz6K8UxzoFmthDCUyZcbXlPIim0LuH7rlgz7ebVW7791lJThZp4WYGHWMiHzhljEfVG9YW4DuEwEmA==} + engines: {node: '>=20'} + cpu: [x64] + os: [win32] + + node-addon-require-builtin@0.1.0: + resolution: {integrity: sha512-HGlhjpNtFP7qtbBIBQ2+eXDe1qXcX4RQa426IMQ+SKoLCQS9AcHYl0kwJCERvG821wfRlJOzGBoREtBOwvUGeg==} + engines: {node: '>=20'} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -5283,29 +5342,31 @@ snapshots: '@chevrotain/types@11.1.2': {} - '@cordisjs/plugin-include@1.0.4(@cordisjs/plugin-loader@1.0.0-rc.4)(cordis@4.0.0-rc.6)': + '@cordisjs/plugin-include@1.0.4(@cordisjs/plugin-loader@1.0.0-rc.5)(cordis@4.0.0-rc.7)': dependencies: - '@cordisjs/plugin-loader': 1.0.0-rc.4(cordis@4.0.0-rc.6) - cordis: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + '@cordisjs/plugin-loader': 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) + cordis: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: 1.8.1 js-yaml: 4.2.0 - '@cordisjs/plugin-include@1.0.4(@cordisjs/plugin-loader@vendor+loader)(cordis@4.0.0-rc.6)': + '@cordisjs/plugin-include@1.0.4(@cordisjs/plugin-loader@vendor+loader)(cordis@4.0.0-rc.7)': dependencies: '@cordisjs/plugin-loader': link:vendor/loader - cordis: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) + cordis: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) cosmokit: 1.8.1 js-yaml: 4.2.0 optional: true - '@cordisjs/plugin-loader@1.0.0-rc.4(cordis@4.0.0-rc.6)': + '@cordisjs/plugin-loader@1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0)': dependencies: - cordis: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + cordis: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: 1.8.1 + optionalDependencies: + node-addon-require-builtin: 0.1.0 - '@cordisjs/plugin-timer@1.1.2(cordis@4.0.0-rc.6)': + '@cordisjs/plugin-timer@1.1.2(cordis@4.0.0-rc.7)': dependencies: - cordis: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + cordis: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: 1.8.1 '@csstools/color-helpers@6.1.0': {} @@ -6308,23 +6369,23 @@ snapshots: convert-source-map@2.0.0: {} - cordis@4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4): + cordis@4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5): dependencies: '@standard-schema/spec': 1.1.0 cosmokit: 1.8.1 optionalDependencies: - '@cordisjs/plugin-include': 1.0.4(@cordisjs/plugin-loader@1.0.0-rc.4)(cordis@4.0.0-rc.6) - '@cordisjs/plugin-loader': 1.0.0-rc.4(cordis@4.0.0-rc.6) + '@cordisjs/plugin-include': 1.0.4(@cordisjs/plugin-loader@1.0.0-rc.5)(cordis@4.0.0-rc.7) + '@cordisjs/plugin-loader': 1.0.0-rc.5(cordis@4.0.0-rc.7)(node-addon-require-builtin@0.1.0) - cordis@4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader): + cordis@4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader): dependencies: '@standard-schema/spec': 1.1.0 cosmokit: 1.8.1 optionalDependencies: - '@cordisjs/plugin-include': 1.0.4(@cordisjs/plugin-loader@vendor+loader)(cordis@4.0.0-rc.6) + '@cordisjs/plugin-include': 1.0.4(@cordisjs/plugin-loader@vendor+loader)(cordis@4.0.0-rc.7) '@cordisjs/plugin-loader': link:vendor/loader - cordis@4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader): + cordis@4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader): dependencies: '@standard-schema/spec': 1.1.0 cosmokit: 1.8.1 @@ -7500,6 +7561,55 @@ snapshots: node-addon-landlock-run-linux-arm64: 0.0.0-test.0 node-addon-landlock-run-linux-x64: 0.0.0-test.0 + node-addon-native-custom-loader@0.1.0: {} + + node-addon-require-builtin-darwin-arm64@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optional: true + + node-addon-require-builtin-darwin-x64@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optional: true + + node-addon-require-builtin-linux-arm64-gnu@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optional: true + + node-addon-require-builtin-linux-x64-gnu@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optional: true + + node-addon-require-builtin-win32-arm64-msvc@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optional: true + + node-addon-require-builtin-win32-ia32-msvc@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optional: true + + node-addon-require-builtin-win32-x64-msvc@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optional: true + + node-addon-require-builtin@0.1.0: + dependencies: + node-addon-native-custom-loader: 0.1.0 + optionalDependencies: + node-addon-require-builtin-darwin-arm64: 0.1.0 + node-addon-require-builtin-darwin-x64: 0.1.0 + node-addon-require-builtin-linux-arm64-gnu: 0.1.0 + node-addon-require-builtin-linux-x64-gnu: 0.1.0 + node-addon-require-builtin-win32-arm64-msvc: 0.1.0 + node-addon-require-builtin-win32-ia32-msvc: 0.1.0 + node-addon-require-builtin-win32-x64-msvc: 0.1.0 + node-domexception@1.0.0: {} node-fetch@3.3.2: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6dc85a6079..bda50a6c69 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,6 +22,7 @@ allowBuilds: # need, so we deny them — install still succeeds. '@google/genai': false protobufjs: false + node-addon-require-builtin: false # The Landlock launcher family is our own sibling-repo release, consumed # fresh (hours old at each coordinated bump) — the release-age quarantine @@ -31,3 +32,7 @@ minimumReleaseAgeExclude: - node-addon-landlock-run - node-addon-landlock-run-linux-arm64 - node-addon-landlock-run-linux-x64 + # Cordis release candidates are source-vendored and pinned in vendor/README.md + # during the same-day sync that updates package manifests and the lockfile. + - '@cordisjs/plugin-loader@1.0.0-rc.5' + - cordis@4.0.0-rc.7 diff --git a/vendor/group/package.json b/vendor/group/package.json index 34a8f59ae2..cefb9288fa 100644 --- a/vendor/group/package.json +++ b/vendor/group/package.json @@ -23,7 +23,7 @@ "author": "Shigma ", "license": "MIT", "peerDependencies": { - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" } } diff --git a/vendor/hmr/package.json b/vendor/hmr/package.json index 28087d5fa8..0b498fc90c 100644 --- a/vendor/hmr/package.json +++ b/vendor/hmr/package.json @@ -35,7 +35,7 @@ }, "peerDependencies": { "@cordisjs/plugin-timer": "^1.1.2", - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "@babel/code-frame": "^7.29.0", diff --git a/vendor/include/package.json b/vendor/include/package.json index f9314d0c5e..c588a33d80 100644 --- a/vendor/include/package.json +++ b/vendor/include/package.json @@ -23,8 +23,8 @@ "author": "Shigma ", "license": "MIT", "peerDependencies": { - "@cordisjs/plugin-loader": "^1.0.0-rc.4", - "cordis": "^4.0.0-rc.6" + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "cordis": "^4.0.0-rc.7" }, "dependencies": { "cosmokit": "^1.8.1", diff --git a/vendor/logger-console/package.json b/vendor/logger-console/package.json index 8c0d8a0bda..7af021c45a 100644 --- a/vendor/logger-console/package.json +++ b/vendor/logger-console/package.json @@ -25,7 +25,7 @@ "author": "Shigma ", "license": "MIT", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "cosmokit": "^1.8.1", diff --git a/vendor/timer/package.json b/vendor/timer/package.json index 07c41150e8..4ae59cd0bd 100644 --- a/vendor/timer/package.json +++ b/vendor/timer/package.json @@ -23,7 +23,7 @@ "author": "Shigma ", "license": "MIT", "peerDependencies": { - "cordis": "^4.0.0-rc.6" + "cordis": "^4.0.0-rc.7" }, "dependencies": { "cosmokit": "^1.8.1" From 52264d87ee2358695646cba9a78cdfc7728d4c4a Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:31:55 +0800 Subject: [PATCH 17/21] fix: test --- .../session-persistence-jsonl/tests/jsonl.spec.ts | 15 ++++++++++++++- .../tests/sqlite.spec.ts | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts b/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts index cef291582a..75fcb48732 100644 --- a/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts +++ b/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts @@ -20,6 +20,19 @@ function mutableHeader(header: SessionHeader): MutableSessionHeader { return header } +async function expectParallelFlushError(promise: Promise, message: RegExp): Promise { + try { + await promise + } catch (error) { + expect(error).toBeInstanceOf(AggregateError) + const [cause] = (error as AggregateError).errors as unknown[] + expect(cause).toBeInstanceOf(Error) + expect((cause as Error).message).toMatch(message) + return + } + throw new Error('expected parallel flush to reject') +} + async function freshRoot(): Promise { const dir = await mkdtemp(join(tmpdir(), 'dsh-jsonl-')) dirs.push(dir) @@ -665,7 +678,7 @@ describe('SessionPersistenceJsonl: edge cases', () => { const backend = ctx2.sessionPersistence as unknown as { materialize: (...args: unknown[]) => Promise } const origMat = backend.materialize.bind(backend) backend.materialize = () => Promise.reject(new Error('disk full')) - await expect(ctx2.parallel('session/flush', session)).rejects.toThrow(/disk full/) + await expectParallelFlushError(ctx2.parallel('session/flush', session), /disk full/) // The events are STILL buffered (not silently dropped): a retry persists them. backend.materialize = origMat await ctx2.parallel('session/flush', session) diff --git a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts index e3429e5835..bae79b0a53 100644 --- a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts +++ b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts @@ -14,6 +14,19 @@ import { runCoordinatorContract, type CoordinatorFixture } from '../../session-p const dirs: string[] = [] afterEach(async () => { for (const d of dirs.splice(0)) await rm(d, { recursive: true, force: true }) }) +async function expectParallelFlushError(promise: Promise, message: RegExp): Promise { + try { + await promise + } catch (error) { + expect(error).toBeInstanceOf(AggregateError) + const [cause] = (error as AggregateError).errors as unknown[] + expect(cause).toBeInstanceOf(Error) + expect((cause as Error).message).toMatch(message) + return + } + throw new Error('expected parallel flush to reject') +} + async function freshDbPath(): Promise { const dir = await mkdtemp(join(tmpdir(), 'dsh-sqlite-')) dirs.push(dir) @@ -405,7 +418,7 @@ describe('SessionPersistenceSqlite: edge cases', () => { }, { inject: ['sessions'] })) session.append('turn/start', { turn: 9, trigger: { kind: 'message', source: { kind: 'user' } } }) await ctx.plugin(SessionPersistenceSqlite, { path }) - await expect(ctx.parallel('session/flush', session)).rejects.toThrow(/id collision/) + await expectParallelFlushError(ctx.parallel('session/flush', session), /id collision/) await ctx.fiber.dispose() }) }) From 33474159e9cdf5f07edb172ec8f355c1997a4a52 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:33:23 +0800 Subject: [PATCH 18/21] docs: add node-addon-internal-loader README --- packages/ui/acp-agent/README.md | 2 +- packages/ui/app-boot/README.md | 4 ++-- packages/ui/app-boot/src/index.ts | 3 ++- packages/ui/stdio-agent/README.md | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ui/acp-agent/README.md b/packages/ui/acp-agent/README.md index 8730102b64..63c9d3df23 100644 --- a/packages/ui/acp-agent/README.md +++ b/packages/ui/acp-agent/README.md @@ -42,7 +42,7 @@ The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the - honors `DSH_SNAPSHOT=replay` by booting the sibling `cordis.snapshot.yml` (the keyless replay tree, `llm-replay` in place of `llm-deepseek`); - in a snapshot run, disposes the context on stdin EOF so the session log is fully flushed before exit. -Run it under `node --expose-internals`: the cordis Loader resolves the config's bare plugin specifiers through its internal module loader, active only under that flag. (`demo:acp` runs under tsx, whose tsconfig `paths` map resolves them instead.) +Run it under `node --expose-internals`, or Loader's optional `node-addon-require-builtin` fallback is required, so the cordis Loader can resolve the config's bare plugin specifiers through its internal module loader. (`demo:acp` runs under tsx, whose tsconfig `paths` map resolves them instead.) All diagnostics go to **stderr** — stdout is the protocol. diff --git a/packages/ui/app-boot/README.md b/packages/ui/app-boot/README.md index fdea712274..73b126c33e 100644 --- a/packages/ui/app-boot/README.md +++ b/packages/ui/app-boot/README.md @@ -12,7 +12,7 @@ Shared boot glue for the app bins ([`dsh-stdio-agent`](../stdio-agent/README.md) Two failure classes the guards handle: `loader.await()` swallows init rejections (`Promise.allSettled`) — Node still exits non-zero on the resulting unhandled rejection, and `installFailLoud` replaces the noisy dump with one labelled line and a guaranteed `exit(1)`; a failed plugin IMPORT is only logged by the Loader (the process would otherwise exit 0 on a usable config typo), leaving a fiber-less entry that `assertEntriesLoaded` turns into a `boot()` rejection. -Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`) resolve through the cordis Loader's internal module loader, active only under `node --expose-internals`; the bins' subprocess smokes exercise that path, while this package's unit suite drives `boot()` in-process against configs with relative specifiers. +Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`) resolve through the cordis Loader's internal module loader, using `node --expose-internals` or the optional `node-addon-require-builtin` fallback. the bins' subprocess smokes exercise that path, while this package's unit suite drives `boot()` in-process against configs with relative specifiers. ## Model Experience @@ -20,6 +20,6 @@ Indirectly, through the plugin tree it loads, which determines the prompts, sche ## Known Limitations and Deferred Work -- **Bare package specifiers depend on Loader internals** — production bins need `node --expose-internals`; an in-process caller without it must use resolvable relative/file specifiers or tsx path mapping. +- **Bare package specifiers depend on Loader internals** — production bins need `node --expose-internals` or the Loader's optional native fallback; an in-process caller without either must use resolvable relative/file specifiers or tsx path mapping. - **Snapshot replay swapping is basename-specific** — only a config ending in `cordis.yml` or `cordis.yaml` maps to the sibling `cordis.snapshot.yml`; custom config names require caller-managed selection. - **Environment loading is cwd-scoped and optional** — the helper loads one `.env` file and warns on failure; it does not search parents, merge profiles, or validate required variables. diff --git a/packages/ui/app-boot/src/index.ts b/packages/ui/app-boot/src/index.ts index a519b3cf70..e270ead589 100644 --- a/packages/ui/app-boot/src/index.ts +++ b/packages/ui/app-boot/src/index.ts @@ -98,7 +98,8 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void { * tree settles. The include uses an absolute file URL while `baseUrl` stays at * the config directory for its relative imports. A missing fiber rejects here; * a later init rejection is handled by {@link installFailLoud}. Built bins need - * `--expose-internals` for bare plugin specifiers; relative specifiers do not. + * `--expose-internals` or the Loader's native fallback for bare plugin + * specifiers; relative specifiers do not. * @param binName - the diagnostic prefix for load-failure errors. * @param absoluteConfigPath - the config to include; must already be absolute * (see {@link resolveConfigPath}). diff --git a/packages/ui/stdio-agent/README.md b/packages/ui/stdio-agent/README.md index 8cd97f5cc7..0f4088ae07 100644 --- a/packages/ui/stdio-agent/README.md +++ b/packages/ui/stdio-agent/README.md @@ -38,7 +38,7 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s ## The bin -`dsh-stdio-agent [path-to-cordis.yml]` (default `./cordis.yml`) loads a gitignored `.env` from the cwd (`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`), then drives the cordis Loader against the config and awaits the whole plugin tree before returning. Run it under `node --expose-internals`: the cordis Loader resolves the config's bare plugin specifiers (`@deepseek-ai/dsh-*`, npm packages) through its internal module loader, which is only active under that flag. The `demo:echo` / `demo:repl` scripts invoke it that way. +`dsh-stdio-agent [path-to-cordis.yml]` (default `./cordis.yml`) loads a gitignored `.env` from the cwd (`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`), then drives the cordis Loader against the config and awaits the whole plugin tree before returning. Run it under `node --expose-internals`, or install the Loader's optional `node-addon-require-builtin` fallback, so the Loader can resolve the config's bare plugin specifiers (`@deepseek-ai/dsh-*`, npm packages). The `demo:echo` / `demo:repl` scripts use `--expose-internals`. ## Example leaf `cordis.yml` From 7fc11d685ae59fb20a90429e637fc794f30688ca Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:51:29 +0800 Subject: [PATCH 19/21] fix: boot use internal loader and fix path resolve --- packages/ui/acp-agent/tests/built-bin.e2e.ts | 6 +++--- packages/ui/app-boot/README.md | 2 +- packages/ui/app-boot/src/index.ts | 19 +++++++++++++------ .../ui/stdio-agent/tests/built-bin.e2e.ts | 6 +++--- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/ui/acp-agent/tests/built-bin.e2e.ts b/packages/ui/acp-agent/tests/built-bin.e2e.ts index c6130130b9..b31082fa23 100644 --- a/packages/ui/acp-agent/tests/built-bin.e2e.ts +++ b/packages/ui/acp-agent/tests/built-bin.e2e.ts @@ -147,11 +147,11 @@ describe.skipIf(!existsSync(acpBin))('dsh-acp-agent BUILT bin (node lib/bin.js, }, 30_000) it('fails LOUD (non-zero exit + stderr) on a config whose directory does not exist', async () => { - // A nonexistent directory prevents even the include plugin import. Loader logs the failure and - // leaves no fiber; boot's settled-entry guard must convert that state into non-zero exit. + // boot() pre-resolves the bootstrap include to an absolute URL, so a nonexistent config + // directory cannot break its import; the include plugin's own read must fail loud instead. const { code, stderr } = await runBinExpectingExit('/nonexistent/dir/cordis.yml') expect(code).not.toBe(0) - expect(stderr).toContain('failed to load') + expect(stderr).toContain('config file not found') }, 30_000) it('fails LOUD (non-zero exit + stderr) on a missing config file in a real directory', async () => { diff --git a/packages/ui/app-boot/README.md b/packages/ui/app-boot/README.md index 73b126c33e..c3deaa4f48 100644 --- a/packages/ui/app-boot/README.md +++ b/packages/ui/app-boot/README.md @@ -8,7 +8,7 @@ Shared boot glue for the app bins ([`dsh-stdio-agent`](../stdio-agent/README.md) | `loadEnv(binName, dir?, warn?)` | Load the gitignored `.env` (Node `process.loadEnvFile`); absent file is fine, an unloadable one warns a single labelled line (default: stderr) | | `installFailLoud(binName, proc?)` | Turn a post-`boot()` unhandled Loader rejection into one labelled stderr line + `exit(1)`; returns the uninstaller (for tests) | | `assertEntriesLoaded(ctx, binName)` | Throw when a settled tree holds an enabled entry with no fiber (a plugin module that failed to import) | -| `boot(binName, absoluteConfigPath)` | Mount the Loader, include the config by absolute `file://` URL, await the whole tree, assert entries loaded, return the root context | +| `boot(binName, absoluteConfigPath)` | Mount the Loader, mount the statically imported include plugin as the `cordis:include` builtin (so the config may live outside `node_modules` reach), include the config by absolute `file://` URL, await the whole tree, assert entries loaded, return the root context | Two failure classes the guards handle: `loader.await()` swallows init rejections (`Promise.allSettled`) — Node still exits non-zero on the resulting unhandled rejection, and `installFailLoud` replaces the noisy dump with one labelled line and a guaranteed `exit(1)`; a failed plugin IMPORT is only logged by the Loader (the process would otherwise exit 0 on a usable config typo), leaving a fiber-less entry that `assertEntriesLoaded` turns into a `boot()` rejection. diff --git a/packages/ui/app-boot/src/index.ts b/packages/ui/app-boot/src/index.ts index e270ead589..3521769816 100644 --- a/packages/ui/app-boot/src/index.ts +++ b/packages/ui/app-boot/src/index.ts @@ -9,6 +9,7 @@ import { pathToFileURL } from 'node:url' import { basename, dirname, resolve } from 'node:path' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' +import Include from '@cordisjs/plugin-include' /** * Resolve the config to boot. Replay swaps a `cordis.yml` basename for @@ -95,11 +96,16 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void { /** * Boot the Loader against `absoluteConfigPath` and return only after the whole - * tree settles. The include uses an absolute file URL while `baseUrl` stays at - * the config directory for its relative imports. A missing fiber rejects here; - * a later init rejection is handled by {@link installFailLoud}. Built bins need - * `--expose-internals` or the Loader's native fallback for bare plugin - * specifiers; relative specifiers do not. + * tree settles. Entry names load through the Loader's internal module loader + * against `baseUrl` (the config directory), which may live outside + * `node_modules` reach and, unbuilt, cannot load vendored source; the + * bootstrap include is therefore statically imported and mounted as the + * `cordis:include` builtin, loading through the ambient module pipeline + * (vite/tsx/plain ESM) while the included tree's own specifiers stay + * config-relative. A missing fiber rejects here; a later init rejection is + * handled by {@link installFailLoud}. Built bins need `--expose-internals` or + * the Loader's native fallback for bare plugin specifiers; relative specifiers + * do not. * @param binName - the diagnostic prefix for load-failure errors. * @param absoluteConfigPath - the config to include; must already be absolute * (see {@link resolveConfigPath}). @@ -109,8 +115,9 @@ export async function boot(binName: string, absoluteConfigPath: string): Promise const ctx = new Context() ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/' await ctx.plugin(Loader) + ctx.loader.builtins.include = Include await ctx.loader.create({ - name: '@cordisjs/plugin-include', + name: 'cordis:include', config: { path: pathToFileURL(absoluteConfigPath).href }, }) await ctx.loader.await() diff --git a/packages/ui/stdio-agent/tests/built-bin.e2e.ts b/packages/ui/stdio-agent/tests/built-bin.e2e.ts index ddd7ca454e..5aa42bbcda 100644 --- a/packages/ui/stdio-agent/tests/built-bin.e2e.ts +++ b/packages/ui/stdio-agent/tests/built-bin.e2e.ts @@ -146,12 +146,12 @@ describe.skipIf(!existsSync(stdioBin))('dsh-stdio-agent BUILT bin (node lib/bin. }, 30_000) it('fails LOUD (non-zero exit + stderr) on a config whose directory does not exist', async () => { - // A nonexistent directory prevents even the include plugin import. Loader leaves no fiber, and - // boot's settled-entry guard must turn that state into a clear non-zero failure. + // boot() pre-resolves the bootstrap include to an absolute URL, so a nonexistent config + // directory cannot break its import; the include plugin's own read must fail loud instead. consumer = await makeConsumer('unused') const { code, stderr } = await runBuiltBin(consumer, '/nonexistent/dir/cordis.yml', '') expect(code).not.toBe(0) - expect(stderr).toContain('failed to load') + expect(stderr).toContain('config file not found') }, 30_000) it('fails LOUD (non-zero exit + stderr) on a missing config file in a real directory', async () => { From e8c31e054d3e50ab4ecdcc811e8fd81df4afea51 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:53:01 +0800 Subject: [PATCH 20/21] fix: pnpm dep after merge --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bfeee5051..95065391d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1415,7 +1415,7 @@ importers: version: link:../user-interaction cordis: specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) packages/ui/stdio-agent: devDependencies: From 600af3ca7986c0af9a463384e4473f6d2f330d5d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:06:08 +0800 Subject: [PATCH 21/21] chore: reconcile loader smoke lockfile --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb5e9a6e00..b1010bce7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1143,7 +1143,7 @@ importers: devDependencies: cordis: specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/support/subagent-mock: dependencies: