/** * DshSessionAdapter — ISessionClient adapter over the dsh session service * (`ctx.sessions`, a `SessionStore` from `@deepseek-ai/dsh-session`). * * Verified dsh surface (`docs/dsh-plugin-contract.md` §4.1): * * SessionStore: create(id?, options?) → Session | prepare(id?, options?) → * Session | enter(session) | announce(session) | flush(session) | * get(id) → Session | undefined | list() → Session[] | * fork(source, boundary?, childSessionId?) → Session * * Session: id, seq, events (readonly SessionEvent[]), surface, header * (cwd, version, ...), append(type, data, opts), deriveMessages(), * requestHeader(), requestContext() * * Mapping notes: * * - `list(directory)` filters sessions whose `header.cwd` matches the * directory (dsh sessions are global — ids are not directory-scoped). * - `messages` maps `deriveMessages()` dsh messages * (`{ id, role, content: ContentBlock[], source }`) into rolebox * `{ info, parts }` messages; ContentBlocks become text/reasoning/tool * parts. * - `todo` / `diff` are extracted best-effort from the session event log * (`todo/write` and `tool/result` events). * - `status` is derived from the event log (`turn/start` / `turn/end`). * * Unsupported operations (dsh has no equivalent on this surface — each is a * documented graceful degradation, matching the Pi adapter's approach): * * - `prompt` / `promptSync` — prompting is driven by the dsh agent loop * (`ctx.agents` / agent inbox), not the SessionStore; return null. * - `abort` — cancellation lives on `Agent.cancel(...)`, not the * SessionStore; return false. * - `compact` — dsh compaction is a data-level `surfaceOp: 'replace'` * append on the session log, not an API; return false. * - `children` — subagent child sessions are listed through * `ctx.subagents.listChildren`, not the SessionStore; return []. * * The dsh session service is consumed structurally (duck-typed). This module * does NOT import `@deepseek-ai/dsh-session` or any `@deepseek-ai/*` package, * and MUST NOT import from `@opencode-ai/*`. * * @module */ import type { ISessionClient } from "../../ports/session-client.ts"; import type { DshContentBlock } from "./agent-registrar.ts"; import type { SessionInfo, Message, FileDiff, Todo, SessionStatus } from "../../types.ts"; /** * Structural `SessionEvent` from `@deepseek-ai/dsh-session` (§4.1). * * rc.6 models `time` as a REQUIRED top-level Unix-epoch-millisecond `number` * on the event envelope (`dsh-session/lib/types/types.d.ts:426`), stamped by * `append()` (`lib/index.js:1456`) — not a nested `{ created }` object and not * an optional `timestamp`/`at`. `seq` is required in rc.6 too, but stays * optional here because rolebox only reads it after a safe-integer guard. */ export interface DshSessionEventLike { readonly type: string; readonly seq?: number; readonly id?: string; readonly sessionID?: string; readonly data?: unknown; readonly time: number; readonly [key: string]: unknown; } /** Structural `Message` from `@deepseek-ai/dsh-llm` (§4.1). */ export interface DshMessageLike { readonly id: string; readonly role: string; readonly content: DshContentBlock[]; readonly source?: unknown; readonly [key: string]: unknown; } /** Structural `Session` from `@deepseek-ai/dsh-session` (§4.1). */ export interface DshSessionLike { readonly id: string; readonly seq: number; readonly events: readonly DshSessionEventLike[]; readonly header?: { readonly cwd?: string; /** On-disk format version (rc.6 `SessionHeader.version`, types.d.ts:46). */ readonly version?: number; readonly [key: string]: unknown; }; append(type: string, data: unknown, opts?: Record): DshSessionEventLike; deriveMessages(): DshMessageLike[]; readonly [key: string]: unknown; } /** Structural `SessionStore` from `@deepseek-ai/dsh-session` (§4.1). */ export interface DshSessionStoreLike { create(id?: string, options?: Record): DshSessionLike; get(id: string): DshSessionLike | undefined; list(): DshSessionLike[]; fork(source: DshSessionLike, boundary?: number, childSessionId?: string): DshSessionLike; flush?(session: DshSessionLike): Promise; } /** * Optional per-session agent-delivery seam for {@link DshSessionAdapter.prompt}. * * dsh (DeepSeek Harness) has NO `prompt` on the SessionStore — prompting is * driven by the live agent loop, so rolebox's graph-notify reminders (which * are delivered through `ISessionClient.prompt`, the SAME path opencode/Pi * use) need a host-way in. On dsh that way is the live `Agent` surface * (`ctx.agents` → `AgentRegistry.get(sessionId)` → one of the agent's * delivery members), which this seam abstracts so the adapter stays SDK-free * (the dsh surface is consumed structurally against the shapes verified in * `docs/dsh-plugin-contract.md` §4.2 — the Agent signature is duck-typed). * * The plugin's injector selects the delivery member from `noReply`, matching * opencode/Pi semantics (`triggerTurn = !noReply`): `noReply: true` uses the * non-waking `inject` member only (:124-132 — queues model-facing context * WITHOUT waking an idle driver); `noReply: false` uses a WAKING member — * `steer` (rc.6 runtime-types.d.ts:116-123: an idle driver starts a turn; a * running driver consumes it at its next step boundary), then `followup` * (:110-115: queues an ordinary follow-up turn and wakes the driver); * `undefined` keeps the legacy best-effort preference (waking preferred, * `inject` fallback). A member the chosen mode requires but the agent does not * expose degrades to `null` rather than silently delivering with the wrong * wake behavior. * * When the plugin provides an injector (wired from an optional `ctx.agents` * probe — the service may be absent in minimal/headless profiles), `prompt()` * routes the reminder into the target session's live agent. Absent → the * adapter keeps its documented no-op (returns `null`), and the graph engine's * F6 notifier logs the degraded reminder instead of crashing. This is * intentionally BEST-EFFORT: the `GraphNotifySource` config is wired on the * dsh path, but a session with no live agent (or a host without `ctx.agents`) * degrades to the same silent-drop marker the engine already records for a * missing emperor session, never a crash. */ export interface DshPromptInjector { /** * Inject a text reminder into a session's live agent. * * @param sessionId - The target dsh session id (the emperor/orchestrator * session that drove `graph_run`, per the graph-notify owner-targeting). * @param text - The `` body (contains the graph * marker + agent). Already carries the resolved agent inline. * @param options - Optional prompt metadata forwarded from * `ISessionClient.prompt` (`agent`, `noReply`). `noReply` SELECTS the * delivery member (see the interface docstring): `true` = non-waking * `inject`, `false` = waking `steer`/`followup`, `undefined` = legacy * best-effort. * @returns A message id (unique per injected message), or `null` when the * session has no live agent / the required delivery member is absent, so * the caller can degrade cleanly. */ inject(sessionId: string, text: string, options?: { agent?: string; noReply?: boolean; }): Promise<{ id: string; } | null>; } /** Options for constructing a {@link DshSessionAdapter}. */ export interface DshSessionAdapterOptions { /** Optional logger name override. */ loggerName?: string; /** * Optional agent-injection seam (see {@link DshPromptInjector}). When * present, `prompt()` routes graph-notify reminders into the target session's * live agent; absent, `prompt()` keeps its documented no-op. */ promptInjector?: DshPromptInjector; } /** * ISessionClient adapter for the dsh platform, backed by a structural * `SessionStore` (the `ctx.sessions` service). */ export declare class DshSessionAdapter implements ISessionClient { readonly store: DshSessionStoreLike; private readonly _log; private readonly _promptInjector?; /** * @param store - The dsh `SessionStore` service (`ctx.sessions`). * @param options - Optional logger name override + agent-injection seam. */ constructor(store: DshSessionStoreLike, options?: DshSessionAdapterOptions); /** * List sessions, optionally filtered to a working directory. * dsh sessions are global; when `directory` is provided only sessions whose * `header.cwd` matches it are returned. */ list(directory?: string): Promise; /** * Get a single session by ID. dsh session ids are global, so `directory` * is ignored for lookup (it only labels the returned SessionInfo). */ get(id: string, directory?: string): Promise; /** * Get messages for a session by mapping `deriveMessages()` into rolebox * `{ info, parts }` messages. ContentBlocks become text / reasoning / tool * parts. Unparseable sessions return []. */ messages(id: string, options?: { directory?: string; limit?: number; }): Promise; /** * Get child sessions — unsupported on the dsh SessionStore. * Subagent child sessions are listed through `ctx.subagents.listChildren` * (a different service), so this always returns []. */ children(_id: string, _directory?: string): Promise; /** * Get todo items by scanning the session event log for `todo/write` events * (`dsh-plugin-contract.md` §4.1 log-only event types). */ todo(id: string, _directory?: string): Promise; /** * Get file diffs by scanning `tool/result` events and parsing their * tool-result text output for JSON `{ file, before, after, ... }` entries * (mirrors the Pi adapter's diff extraction). */ diff(id: string, options?: { directory?: string; messageID?: string; }): Promise; /** * Get the status of a session, derived from its event log: the last * `turn/start`/`turn/end` pair decides `busy` vs `idle`. Unknown sessions * return null. */ status(id: string, _directory?: string): Promise; /** * Fork a session. The new session's id is assigned by dsh; the fork's * `parentID` is set to the source id. When `options.messageID` is provided * it is resolved to the rc.6 fork boundary — the inclusive source event * `seq` of the event carrying that message id (`types/index.d.ts:413`; * `lib/index.js:1858-1861`). An id that matches no event is refused * (`null`) rather than silently forking at the source's last event. */ fork(id: string, options?: { directory?: string; messageID?: string; }): Promise; /** * Create a new session via the dsh SessionStore. * * rc.6 folds `meta.cwd` / `meta.parentSession` into the persisted * `SessionHeader` (`types/types.d.ts:84-100`; `lib/index.js:1653-1663`); a * top-level `{directory}` is ignored. `directory` is therefore forwarded as * `meta.cwd` — so `header.cwd`, and thus `list(directory)` filtering, agree — * and `parentID` as `meta.parentSession` for durable lineage across reload. * Both are also recorded on the returned SessionInfo. Returns null when dsh * rejects creation. */ create(options: { directory: string; agent?: string; parentID?: string; }): Promise; /** * Abort a running session — unsupported on the dsh SessionStore. * dsh cancellation lives on the agent (`Agent.cancel(cause, opts)`), which * is not reachable through this surface; return false. */ abort(_id: string): Promise; /** * Compact a session's context — unsupported. dsh compaction is a * data-level `surfaceOp: 'replace'` append on the session log, not a * SessionStore API; return false. */ compact(_id: string): Promise; /** * Prompt a session asynchronously (fire-and-forget). * * The dsh SessionStore has no `prompt`: prompting is driven by the live * agent loop. When the adapter was constructed with a {@link DshPromptInjector} * (the plugin wires one from the optional `ctx.agents` live-agent registry), * this routes the prompt's text into the target session's agent — the dsh * equivalent of opencode/Pi's `sessionClient.prompt` used by graph-notify to * deliver a `` to the orchestrator. The injector selects the * delivery member from `options.noReply` (matching opencode/Pi * `triggerTurn = !noReply`): `true` uses the non-waking `inject` member, * `false` uses a waking `steer`/`followup` member, `undefined` keeps the * legacy best-effort preference (rc.6 runtime-types.d.ts:110-132). * * No injector (or an injection that fails / finds no live agent) degrades * cleanly: the reminder is dropped the same way a missing emperor session is * dropped (the graph engine's F6 notifier logs a degraded marker), and the * adapter returns `null` to signal the caller no prompt was enqueued. */ prompt(id: string, options: { parts: Array<{ type: string; text: string; }>; noReply?: boolean; system?: string; agent?: string; model?: { providerID: string; modelID: string; }; fromLoop?: boolean; }): Promise<{ id: string; } | null>; /** * Prompt a session synchronously — unsupported (see `prompt`); return null. */ promptSync(_id: string, _options: { parts: Array<{ type: string; text: string; }>; agent?: string; signal?: AbortSignal; }): Promise<{ parts: Array<{ type: string; text?: string; }>; } | null>; /** * Resolve a rolebox message id to the rc.6 fork boundary — the inclusive * source event `seq` of the event that carries that message. rc.6 `fork` * takes a numeric seq, not a message id (`types/index.d.ts:413`; * `lib/index.js:1858-1861`), so a caller's `messageID` must be mapped * through the event log first. Scans the REAL rc.6 event data shapes * (`types/types.d.ts:262,278,306`): * * - `user/message` → `data.id` * - `assistant/message` → `data.message.id` * - `tool/result` → `data.message.id` * * @returns The matching event's `seq`, or `undefined` when no event carries * the given message id (the caller decides how to degrade). */ private resolveBoundarySeq; /** True when the session's `header.cwd` matches the given directory. */ private matchesDirectory; /** * Map a dsh Session into a rolebox SessionInfo. Title is derived from the * first user message text; `time` is the min/max event `time` (rc.6 stamps * every event with a required top-level `time: number`). */ private toSessionInfo; /** * Map a dsh Message into a rolebox `{ info, parts }` message. * * Message time is taken from the owning session event's `time` (via * `messageTimes`), because rc.6 `Message` carries no `timestamp` and no * source `time` (`dsh-llm/lib/types/message.d.ts:120-128`). A message id * absent from the event log (e.g. a synthetic/derived message) gets `0` — * there is no reliable timestamp to fall back to, and inventing `Date.now()` * would silently fabricate a time. */ private toMessage; /** * Map a dsh ContentBlock into a rolebox message Part. * * A `tool-call` block's `arguments` is a raw JSON string and is parsed into * the part's `state.input`. A `tool-result` block has no `name` in rc.6, so * its label is resolved by pairing `toolCallId` against the `tool-call` * blocks in the same derived message set (`toolCallNames`); an unpaired * result is labeled `"unknown"`. */ private toPart; } //# sourceMappingURL=session.d.ts.map