dsh-loop-engine 1.0.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +151 -0
  2. package/README.zh.md +93 -0
  3. package/lib/client.js +403 -0
  4. package/lib/index.js +4310 -0
  5. package/lib/invariant.js +83 -0
  6. package/lib/types/client/LoopEngineBadge.d.ts +34 -0
  7. package/lib/types/client/LoopEngineSection.d.ts +34 -0
  8. package/lib/types/client/index.d.ts +28 -0
  9. package/lib/types/client/locales.d.ts +40 -0
  10. package/lib/types/client/store.d.ts +45 -0
  11. package/lib/types/commands.d.ts +32 -0
  12. package/lib/types/driver-core/ownership.d.ts +41 -0
  13. package/lib/types/driver-core/permission-knobs.d.ts +26 -0
  14. package/lib/types/driver-core/prompt.d.ts +23 -0
  15. package/lib/types/driver-core/skill-inject.d.ts +59 -0
  16. package/lib/types/engine-claude/agent.d.ts +102 -0
  17. package/lib/types/engine-claude/loop.d.ts +89 -0
  18. package/lib/types/engine-claude/mapping.d.ts +83 -0
  19. package/lib/types/engine-claude/permission.d.ts +41 -0
  20. package/lib/types/engine-claude/process.d.ts +59 -0
  21. package/lib/types/engine-claude/sdk.d.ts +57 -0
  22. package/lib/types/engine-claude/types.d.ts +18 -0
  23. package/lib/types/engine-codex/agent.d.ts +109 -0
  24. package/lib/types/engine-codex/appserver/client.d.ts +49 -0
  25. package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
  26. package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
  27. package/lib/types/engine-codex/appserver/types.d.ts +215 -0
  28. package/lib/types/engine-codex/loop.d.ts +92 -0
  29. package/lib/types/engine-codex/permission.d.ts +32 -0
  30. package/lib/types/engine-codex/skills.d.ts +26 -0
  31. package/lib/types/engine-codex/types.d.ts +19 -0
  32. package/lib/types/engine-pi/agent.d.ts +125 -0
  33. package/lib/types/engine-pi/loop.d.ts +96 -0
  34. package/lib/types/engine-pi/permission.d.ts +43 -0
  35. package/lib/types/engine-pi/rpc/client.d.ts +105 -0
  36. package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
  37. package/lib/types/engine-pi/rpc/types.d.ts +235 -0
  38. package/lib/types/engine-pi/skills.d.ts +26 -0
  39. package/lib/types/engine-pi/types.d.ts +27 -0
  40. package/lib/types/index.d.ts +96 -0
  41. package/lib/types/invariant.d.ts +23 -0
  42. package/lib/types/namespace.d.ts +9 -0
  43. package/lib/types/patch-manager.d.ts +47 -0
  44. package/lib/types/settings.d.ts +29 -0
  45. package/lib/types/skills.d.ts +77 -0
  46. package/package.json +103 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Strict-LF JSONL client for the Pi RPC subprocess (`pi --mode rpc`).
3
+ *
4
+ * The driver hands the client a process handle carrying its stdin/stdout/stderr
5
+ * (projected from the dsh subprocess seam, so the whole `pi` child is sandboxed
6
+ * by the harness). The client frames records on a bare `\n` only — not on
7
+ * Unicode separators — using a byte decoder, tolerates a trailing `\r`, and
8
+ * correlates command responses by the optional `id` field while dispatching
9
+ * every non-response line to a buffered event stream.
10
+ *
11
+ * @module dsh-loop-engine/engine-pi/rpc/client
12
+ */
13
+ import type { ChildProcess } from 'node:child_process';
14
+ import type { Readable, Writable } from 'node:stream';
15
+ import type { PiCommand, PiEvent, PiResponse } from './types.ts';
16
+ /** A spawned Pi RPC process as the protocol transport needs it. */
17
+ export interface PiProcess {
18
+ /** Child stdin (command JSON lines). */
19
+ readonly stdin: Writable;
20
+ /** Child stdout (response + event JSON lines). */
21
+ readonly stdout: Readable;
22
+ /** Child stderr (diagnostics; buffered and dropped). */
23
+ readonly stderr: Readable;
24
+ /** Register a single human-readable termination callback. */
25
+ onExit(handler: () => void): void;
26
+ /** Request process-tree termination. */
27
+ terminate(): void;
28
+ }
29
+ /** The exact argv/cwd/env the driver requests for one `pi --mode rpc` child. */
30
+ export interface PiSpawnSpec {
31
+ /** The program plus its flags; `argv[0]` is the Pi CLI entrypoint. */
32
+ readonly argv: readonly string[];
33
+ readonly cwd: string;
34
+ readonly env: Record<string, string>;
35
+ }
36
+ /** Spawns one Pi RPC process over the given spec (the driver's spawn capability). */
37
+ export type PiSpawnCapability = (spec: PiSpawnSpec) => PiProcess;
38
+ /** Callback receiving every non-response event line. */
39
+ export type PiEventHandler = (event: PiEvent) => void;
40
+ /** Options for one `prompt` command. */
41
+ export interface PiPromptOptions {
42
+ readonly streamingBehavior?: 'steer' | 'followUp';
43
+ }
44
+ /** Project a `node:child_process` child onto the Pi protocol transport. */
45
+ export declare function fromChildProcess(child: ChildProcess): PiProcess;
46
+ /** Prompt the agent and stream its events. */
47
+ export declare class PiRpcClient {
48
+ private readonly process;
49
+ private reqId;
50
+ private pending;
51
+ private readonly eventBuffer;
52
+ private eventWake;
53
+ private eventHandler;
54
+ private disposed;
55
+ private readonly decoder;
56
+ private buffer;
57
+ private readonly onStderr;
58
+ /** Whether this client was disposed or its process exited. */
59
+ get closed(): boolean;
60
+ /** Mount a client over an already-spawned Pi RPC process. */
61
+ constructor(process: PiProcess);
62
+ /**
63
+ * Create a client, spawning the Pi RPC child through the supplied capability
64
+ * (or the default node-runtime spawn when none is given).
65
+ * @param spec - the Pi CLI argv/cwd/env the child should run with.
66
+ * @param spawn - optional process-spawn capability (the subprocess seam);
67
+ * absent falls back to the plain node child spawn.
68
+ * @returns the connected client.
69
+ */
70
+ static create(spec: PiSpawnSpec, spawn?: PiSpawnCapability): PiRpcClient;
71
+ /** Register the event dispatch handler. */
72
+ onEvent(handler: PiEventHandler): void;
73
+ /** Drop any events still buffered from a previous step (stateless per-step sessions). */
74
+ clearEvents(): void;
75
+ /** Start a fresh Pi session. */
76
+ newSession(): Promise<PiResponse>;
77
+ /** Prompt the agent and await the acceptance response. */
78
+ prompt(message: string, options?: PiPromptOptions): Promise<PiResponse>;
79
+ /** Abort the current agent operation. */
80
+ abort(): Promise<PiResponse>;
81
+ /** Query session stats. */
82
+ getSessionStats(): Promise<PiResponse>;
83
+ /** Send a command without awaiting its response (fire-and-forget). */
84
+ send(command: PiCommand): void;
85
+ /**
86
+ * Send a command and await the correlated response. Assigns a fresh `id`
87
+ * when the command carries none, so responses always round-trip.
88
+ */
89
+ request(command: PiCommand): Promise<PiResponse>;
90
+ /**
91
+ * Consume every buffered event as an async generator, waking as fresh lines
92
+ * arrive. The caller bounds the iteration by a terminal event; unmatched
93
+ * lines stay buffered for a later iteration.
94
+ */
95
+ events(): AsyncGenerator<PiEvent, void, void>;
96
+ /** Dispose the client and request child termination. */
97
+ dispose(): void;
98
+ /** Feed one chunk of stdout into the framing state machine. */
99
+ private feed;
100
+ /** Dispatch one parsed line to the pending map or the event queue. */
101
+ private dispatch;
102
+ /** Drain decoded stderr bytes (no-op consumer keeps the pipe flowing). */
103
+ private consumeStderr;
104
+ }
105
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Maps Pi RPC messages and end-of-execution events to dsh session-log events.
3
+ * Token-level streaming deltas are folded inline by the driver's step loop
4
+ * (they carry live progress); this module projects the end-state items — a
5
+ * completed tool call, a completed tool execution, and a finished turn's usage
6
+ * — into the durable `tool/call`, `tool/result`, and usage events.
7
+ *
8
+ * @module dsh-loop-engine/engine-pi/rpc/mapping
9
+ */
10
+ import type { TokenUsage, ToolResultMessage } from '@deepseek-ai/dsh-llm';
11
+ import type { PiUsage } from './types.ts';
12
+ /** Map one Pi usage snapshot to dsh TokenUsage. */
13
+ export declare function mapUsage(usage: PiUsage): TokenUsage;
14
+ /**
15
+ * Derive the compact transcript text of a Pi content block, joining nested
16
+ * text segments so the durable tool-result block carries the read model text.
17
+ * @param content - the result payload (e.g. `{ content: [{ type, text }, ...] }`).
18
+ * @returns the joined text.
19
+ */
20
+ export declare function resultText(content: unknown): string;
21
+ /** Map a completed Pi tool-execution end event to the durable tool/result message. */
22
+ export declare function mapToolResult(ev: {
23
+ toolCallId: string;
24
+ result: unknown;
25
+ isError: boolean;
26
+ }): ToolResultMessage;
27
+ /** Map the identity of a Pi message tool call or execution start to a durable tool/call. */
28
+ export declare function mapToolCall(ev: {
29
+ callId: string;
30
+ name: string;
31
+ arguments: unknown;
32
+ }): {
33
+ callId: string;
34
+ name: string;
35
+ arguments: string;
36
+ };
37
+ //# sourceMappingURL=mapping.d.ts.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Pi RPC protocol type definitions. A minimal subset of the upstream
3
+ * `pi --mode rpc` protocol, covering only what the driver needs: the commands
4
+ * it sends (`new_session`, `prompt`, `abort`, `get_session_stats`), the
5
+ * response envelope, and the streaming events it maps into the durable dsh
6
+ * session log. Types only — no runtime code.
7
+ *
8
+ * The protocol is strict LF (`\n`) JSONL: records are delimited only by a bare
9
+ * `\n` (a trailing `\r` is tolerated), and Unicode separators such as U+2028 /
10
+ * U+2029 are ordinary characters inside JSON strings — so a generic line reader
11
+ * that treats them as newlines is not compliant.
12
+ *
13
+ * @module dsh-loop-engine/engine-pi/rpc/types
14
+ */
15
+ /** Optional per-command correlation id; echoed back on the response. */
16
+ export interface PiCommandCorrelation {
17
+ readonly id?: number;
18
+ }
19
+ /** Start a fresh Pi session (the driver issues one per dsh step). */
20
+ export interface PiNewSessionCommand extends PiCommandCorrelation {
21
+ readonly type: 'new_session';
22
+ readonly parentSession?: string;
23
+ }
24
+ /** Send a user prompt to the agent and begin streaming events. */
25
+ export interface PiPromptCommand extends PiCommandCorrelation {
26
+ readonly type: 'prompt';
27
+ readonly message: string;
28
+ readonly images?: readonly PiImage[];
29
+ readonly streamingBehavior?: 'steer' | 'followUp';
30
+ }
31
+ /** Abort the current agent operation. */
32
+ export interface PiAbortCommand extends PiCommandCorrelation {
33
+ readonly type: 'abort';
34
+ }
35
+ /** Query session stats (usage/cost fallback when a message carries none). */
36
+ export interface PiGetSessionStatsCommand extends PiCommandCorrelation {
37
+ readonly type: 'get_session_stats';
38
+ }
39
+ /** Every command the client can send. */
40
+ export type PiCommand = PiNewSessionCommand | PiPromptCommand | PiAbortCommand | PiGetSessionStatsCommand;
41
+ /** Message attachment (images) accepted by `prompt`. */
42
+ export interface PiImage {
43
+ readonly type: 'image';
44
+ readonly data: string;
45
+ readonly mimeType: string;
46
+ }
47
+ /** Successful or failed command response. */
48
+ export interface PiResponse {
49
+ readonly type: 'response';
50
+ readonly command?: string;
51
+ readonly success: boolean;
52
+ readonly error?: string;
53
+ readonly id?: number;
54
+ readonly data?: unknown;
55
+ }
56
+ /** Session statistics returned by `get_session_stats`. */
57
+ export interface PiSessionStats {
58
+ readonly tokens?: {
59
+ readonly input?: number;
60
+ readonly output?: number;
61
+ readonly cacheRead?: number;
62
+ readonly cacheWrite?: number;
63
+ readonly total?: number;
64
+ };
65
+ readonly contextUsage?: {
66
+ readonly tokens?: number | null;
67
+ readonly contextWindow?: number;
68
+ readonly percent?: number | null;
69
+ };
70
+ }
71
+ /** Provider-reported token usage attached to messages and updates. */
72
+ export interface PiUsage {
73
+ readonly input?: number;
74
+ readonly output?: number;
75
+ readonly cacheRead?: number;
76
+ readonly cacheWrite?: number;
77
+ readonly totalTokens?: number;
78
+ }
79
+ /** A content block of a Pi message. */
80
+ export type PiContent = {
81
+ readonly type: 'text';
82
+ readonly text: string;
83
+ } | {
84
+ readonly type: 'thinking';
85
+ readonly thinking: string;
86
+ } | {
87
+ readonly type: 'toolCall';
88
+ readonly id: string;
89
+ readonly name: string;
90
+ readonly arguments: unknown;
91
+ };
92
+ /** One role-tagged Pi message. */
93
+ export interface PiMessage {
94
+ readonly role: 'user' | 'assistant' | 'toolResult' | 'system';
95
+ readonly content: string | readonly PiContent[];
96
+ readonly usage?: PiUsage;
97
+ readonly isError?: boolean;
98
+ readonly toolCallId?: string;
99
+ readonly toolName?: string;
100
+ readonly timestamp?: number;
101
+ readonly id?: string;
102
+ }
103
+ /** A tool result as carried by `turn_end.toolResults`. */
104
+ export interface PiToolResult {
105
+ readonly role: 'toolResult';
106
+ readonly toolCallId: string;
107
+ readonly toolName: string;
108
+ readonly content: readonly PiContent[];
109
+ readonly isError?: boolean;
110
+ readonly usage?: PiUsage;
111
+ }
112
+ /** The `assistantMessageEvent` delta union of `message_update`. */
113
+ export type PiAssistantMessageEvent = {
114
+ readonly type: 'text_start';
115
+ readonly contentIndex: number;
116
+ } | {
117
+ readonly type: 'text_delta';
118
+ readonly contentIndex: number;
119
+ readonly delta: string;
120
+ } | {
121
+ readonly type: 'text_end';
122
+ readonly contentIndex: number;
123
+ readonly content?: string;
124
+ } | {
125
+ readonly type: 'thinking_start';
126
+ readonly contentIndex: number;
127
+ } | {
128
+ readonly type: 'thinking_delta';
129
+ readonly contentIndex: number;
130
+ readonly delta: string;
131
+ } | {
132
+ readonly type: 'thinking_end';
133
+ readonly contentIndex: number;
134
+ readonly thinking?: string;
135
+ } | {
136
+ readonly type: 'toolcall_start';
137
+ readonly contentIndex: number;
138
+ readonly id: string;
139
+ readonly toolName: string;
140
+ } | {
141
+ readonly type: 'toolcall_delta';
142
+ readonly contentIndex: number;
143
+ readonly delta: string;
144
+ } | {
145
+ readonly type: 'toolcall_end';
146
+ readonly contentIndex: number;
147
+ readonly toolCall: {
148
+ readonly id: string;
149
+ readonly name: string;
150
+ readonly arguments: unknown;
151
+ };
152
+ };
153
+ /** An `extension_ui_request` (dialog or fire-and-forget). */
154
+ export interface PiExtensionUiRequest {
155
+ readonly type: 'extension_ui_request';
156
+ readonly id: string;
157
+ readonly method: 'select' | 'confirm' | 'input' | 'editor' | 'notify' | 'setStatus' | 'setWidget' | 'setTitle' | 'set_editor_text';
158
+ readonly title?: string;
159
+ readonly options?: readonly string[];
160
+ readonly message?: string;
161
+ readonly [key: string]: unknown;
162
+ }
163
+ /** A tool-execution event (start / update / end). */
164
+ export type PiToolExecutionEvent = {
165
+ readonly type: 'tool_execution_start';
166
+ readonly toolCallId: string;
167
+ readonly toolName: string;
168
+ readonly args: unknown;
169
+ } | {
170
+ readonly type: 'tool_execution_update';
171
+ readonly toolCallId: string;
172
+ readonly toolName: string;
173
+ readonly args: unknown;
174
+ readonly partialResult: unknown;
175
+ } | {
176
+ readonly type: 'tool_execution_end';
177
+ readonly toolCallId: string;
178
+ readonly toolName: string;
179
+ readonly result: unknown;
180
+ readonly isError: boolean;
181
+ };
182
+ /** Every agent event the driver consumes or ignores. */
183
+ export type PiEvent = {
184
+ readonly type: 'response';
185
+ } & PiResponse | {
186
+ readonly type: 'agent_start';
187
+ } | {
188
+ readonly type: 'agent_end';
189
+ readonly messages?: readonly PiMessage[];
190
+ readonly willRetry?: boolean;
191
+ } | {
192
+ readonly type: 'agent_settled';
193
+ } | {
194
+ readonly type: 'turn_start';
195
+ } | {
196
+ readonly type: 'turn_end';
197
+ readonly message?: PiMessage;
198
+ readonly toolResults?: readonly PiToolResult[];
199
+ } | {
200
+ readonly type: 'message_start';
201
+ readonly message: PiMessage;
202
+ } | {
203
+ readonly type: 'message_update';
204
+ readonly usage?: PiUsage;
205
+ readonly assistantMessageEvent: PiAssistantMessageEvent;
206
+ } | {
207
+ readonly type: 'message_end';
208
+ readonly message: PiMessage;
209
+ } | PiToolExecutionEvent | {
210
+ readonly type: 'compaction_start';
211
+ readonly reason?: string;
212
+ } | {
213
+ readonly type: 'compaction_end';
214
+ readonly reason?: string;
215
+ readonly aborted?: boolean;
216
+ readonly willRetry?: boolean;
217
+ readonly result?: unknown;
218
+ } | {
219
+ readonly type: 'auto_retry_start';
220
+ readonly attempt?: number;
221
+ } | {
222
+ readonly type: 'auto_retry_end';
223
+ readonly success?: boolean;
224
+ readonly attempt?: number;
225
+ readonly finalError?: string;
226
+ } | {
227
+ readonly type: 'queue_update';
228
+ readonly steering?: readonly string[];
229
+ readonly followUp?: readonly string[];
230
+ } | {
231
+ readonly type: 'bash_execution_update';
232
+ readonly id?: string;
233
+ readonly delta?: string;
234
+ } | PiExtensionUiRequest;
235
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Pi skill provider: exposes the Pi CLI's instruction files as DSH skills.
3
+ * Pi reads project and user instruction files named `AGENTS.md` (like codex),
4
+ * and its own tool rules live under `.pi/`. Each found `AGENTS.md` is surfaced
5
+ * as a single user-invocable skill whose content is the file body, so the dsh
6
+ * skill-injection seam (`/name` gestures) can carry it into the prompt — the
7
+ * same bridge the codex driver exposes.
8
+ *
9
+ * @module dsh-loop-engine/engine-pi/skills
10
+ */
11
+ import { type SkillCandidate, type SkillDefinition, type SkillLookupOptions, type SkillProvider, type SkillProviderControl } from '../skills.ts';
12
+ /**
13
+ * Skill provider that discovers `AGENTS.md` from the project root (git root
14
+ * when one exists) and the user home `~/.pi/AGENTS.md`.
15
+ */
16
+ export declare class PiSkillProvider implements SkillProvider {
17
+ private readonly control;
18
+ readonly name = "pi";
19
+ constructor(control: SkillProviderControl);
20
+ list(options: SkillLookupOptions): Promise<readonly SkillCandidate[]>;
21
+ get(candidate: SkillCandidate, _options: SkillLookupOptions): Promise<SkillDefinition | undefined>;
22
+ /** Read one AGENTS.md file and push a candidate when it exists. */
23
+ private collectAgentsMd;
24
+ }
25
+ export default PiSkillProvider;
26
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Public types of the Pi loop driver. Types only — no runtime code.
3
+ *
4
+ * Pi carries no native permission system ("runs with the permissions of the
5
+ * user"), so the declarative stance this driver resolves is a sandbox mode plus
6
+ * the tool set the process is allowed to use; the rest of the driver then
7
+ * either wraps the whole `pi --mode rpc` child in the dsh subprocess sandbox or
8
+ * prunes its `--tools` accordingly.
9
+ *
10
+ * @module dsh-loop-engine/engine-pi/types
11
+ */
12
+ /** Pi sandbox stances the driver can resolve, mapped from the dsh session knobs. */
13
+ export type PiSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
14
+ /** Driver configuration after defaults and load-time validation. */
15
+ export interface ResolvedConfig {
16
+ /** Pinned sandbox mode; `undefined` follows the session's dsh permission knobs per query. */
17
+ readonly sandboxMode: PiSandboxMode | undefined;
18
+ /** LLM provider the `pi` RPC process is launched with (`--provider`). */
19
+ readonly provider: string | undefined;
20
+ /** Model pattern the `pi` RPC process is launched with (`--model`). */
21
+ readonly model: string | undefined;
22
+ /** Thinking/reasoning level for the model (`--model <id>:<level>` or set at runtime). */
23
+ readonly thinkingLevel: string | undefined;
24
+ /** Explicit environment entries layered over the credential-scrubbed parent environment. */
25
+ readonly env: Record<string, string>;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Web-switchable agent loop engine, node half.
3
+ *
4
+ * Hosts the non-default agent-loop engines (Claude Code, Codex) and
5
+ * bridges them with the harness's single AgentFactory slot. The engine is
6
+ * selected by the `agent-loop-engine` settings section; the selection is
7
+ * realized by a managed block in the profile's `cordis.patch.yml` that
8
+ * disables the base bundle's `agent-loop` row — exactly one AgentFactory may
9
+ * register, so a non-default engine owns the slot by disabling the base loop
10
+ * first, and `in-process` leaves the base row active (this plugin does NOT
11
+ * register its own factory then).
12
+ *
13
+ * The managed block is the ground truth the factory decision reads at boot:
14
+ * apply() reads the file synchronously, so a committed engine change takes
15
+ * effect on the next recomposition (restart); the config-only HMR watcher
16
+ * re-applies the patch file but cannot re-register an AgentFactory mid-run.
17
+ * The settings section is seeded from the block so the UI mirrors the file,
18
+ * and a committed settings change writes the block (only when it differs).
19
+ *
20
+ * @module dsh-loop-engine
21
+ */
22
+ import { Context } from '@deepseek-ai/cordis';
23
+ import z from '@deepseek-ai/schemastery';
24
+ import { type Config as ClaudeCodeConfig } from './engine-claude/loop.ts';
25
+ import type { CodexApprovalPolicy, CodexSandboxMode } from './engine-codex/types.ts';
26
+ import { type LoopEngineId } from './settings.ts';
27
+ export declare const name = "loop-engine";
28
+ /**
29
+ * Services the plugin's own fiber requires. The plugin declares none of its
30
+ * own: the optional host services it reads (`commands`, `skills`) are resolved
31
+ * lazily via `ctx.get` and may be absent, and the hosted engine factories
32
+ * (Claude Code / Codex) declare their own `inject` when the plugin mounts them
33
+ * as children. Empty keeps the plugin from demanding a service that a minimal
34
+ * profile does not provide.
35
+ */
36
+ export declare const inject: never[];
37
+ /** Composition entry for the loop engine selection and the hosted engine drivers. */
38
+ export interface Config extends ClaudeCodeConfig {
39
+ /** Profile whose `cordis.patch.yml` carries the managed block; defaults to `web`. */
40
+ profile?: string;
41
+ /** Patch file name inside the profile; defaults to `cordis.patch.yml`. */
42
+ patchFilename?: string;
43
+ /** Explicit absolute path to the patch file, overriding profile + filename. */
44
+ patchPath?: string;
45
+ /** Pinned Codex sandbox mode; falls back to the session's dsh permission knobs. */
46
+ sandboxMode?: CodexSandboxMode;
47
+ /** Pinned Codex approval policy; falls back to the session's dsh permission knobs. */
48
+ approvalPolicy?: CodexApprovalPolicy;
49
+ /** LLM provider for the Pi RPC child (`--provider`). */
50
+ piProvider?: string;
51
+ /** Thinking/reasoning level for the Pi RPC child, appended to its `--model`. */
52
+ piThinking?: string;
53
+ }
54
+ /**
55
+ * Schema of the loop engine composition entry.
56
+ *
57
+ * A schemastery object validates each field only when it is present and lets
58
+ * an absent key fall through as `undefined`, so omitted knobs are accepted —
59
+ * matching the permissive interface and read path (`resolvePatchPath` defaults
60
+ * the patch path; each engine driver resolves only the knobs it owns and
61
+ * omitted deployment tunables fall back to the session). The composition entry
62
+ * is an engine-agnostic superset: the selectable knobs belong to whichever
63
+ * engine the settings pick at runtime, so both engines' knobs may coexist and
64
+ * only the selected one is consumed.
65
+ */
66
+ export declare const Config: z<Config>;
67
+ /** Resolve the managed patch file from configuration, defaulting to the web profile. */
68
+ export declare function resolvePatchPath(config: Config): string;
69
+ /** Atomically replace the patch file (same-directory temp + rename). */
70
+ export declare function writePatchFile(path: string, text: string): Promise<void>;
71
+ /**
72
+ * Synchronously atomically replace the patch file. The engine-selection
73
+ * onChange is a synchronous hook with no await, and the write MUST land before
74
+ * the caller is told the switch committed — otherwise a user who restarts
75
+ * `dsh web` immediately reads the stale file and the previous engine boots.
76
+ * @param path - the profile's patch file.
77
+ * @param text - the next file content.
78
+ */
79
+ export declare function writePatchFileSync(path: string, text: string): void;
80
+ /**
81
+ * Rewrite the managed block for a target engine, preserving the rest of the
82
+ * file byte for byte. Only writes when the file actually differs.
83
+ * @param path - the profile's patch file.
84
+ * @param engine - the target engine.
85
+ * @returns whether a write occurred.
86
+ */
87
+ export declare function syncManagedBlock(path: string, engine: LoopEngineId): Promise<boolean>;
88
+ /**
89
+ * Apply the plugin: seed the settings section from the managed block, host
90
+ * the non-default engine factory when the block says so, and translate
91
+ * committed engine changes into managed-block writes.
92
+ * @param ctx - the composing context.
93
+ * @param config - composition entry for the managed patch file.
94
+ */
95
+ export declare function apply(ctx: Context, config: Config): void;
96
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Package-owned invariant companion for the loop engine selection.
3
+ *
4
+ * The plugin's owned relationship is the patch-manager round trip: rendering
5
+ * a managed block for an engine and reading it back must produce the same
6
+ * engine, and the `in-process` engine must render an absent block (so the base
7
+ * bundle's `agent-loop` row stays mounted). The companion asserts both against
8
+ * the pure transform, binding the writer's inverse to the reader directly.
9
+ *
10
+ * @module dsh-loop-engine/invariant
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ /** Cordis companion plugin name. */
14
+ export declare const name = "loop-engine-invariant";
15
+ /** Services required before the companion can register. */
16
+ export declare const inject: string[];
17
+ /**
18
+ * Register the loop-engine invariant contribution.
19
+ * @param ctx - Cordis context carrying the invariant service.
20
+ * @returns the installed registration's disposer after setup succeeds.
21
+ */
22
+ export declare const apply: (ctx: Context) => Promise<() => void>;
23
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Loop engine namespace literal: the one string both halves agree on, in a
3
+ * module with no runtime imports so the browser bundle can import it without
4
+ * dragging `dsh-settings` (a host-side service) into the client artifact.
5
+ * @module dsh-loop-engine/namespace
6
+ */
7
+ /** Settings namespace carrying the deployment's selected agent loop engine. */
8
+ export declare const LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
9
+ //# sourceMappingURL=namespace.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Managed-block editing for a profile's `cordis.patch.yml`.
3
+ *
4
+ * The plugin owns one contiguous block inside the user's patch file, delimited
5
+ * by a begin/end marker pair, and rewrites only that span on engine switches
6
+ * — everything else the user wrote (other patches, their comments) survives
7
+ * byte for byte. The block's content is the loader patch that takes the loop
8
+ * engine over: it disables the base bundle's `agent-loop` row so this plugin's
9
+ * factory (hosted by dsh-loop-engine) can register without colliding, because
10
+ * the harness admits exactly one AgentFactory:
11
+ *
12
+ * # -- dsh-loop-engine managed block: claude-code --
13
+ * - id: agent-loop
14
+ * disabled: true
15
+ * # -- /dsh-loop-engine managed block --
16
+ *
17
+ * `in-process` renders an absent block (the base bundle's `agent-loop` row
18
+ * stays active and supplies the factory), so switching back removes the span
19
+ * entirely. Any other engine renders the same disable block, and the begin
20
+ * marker carries the specific engine id (`# -- dsh-loop-engine managed block:
21
+ * claude-code --`) so `currentEngineOf` can read which non-default engine owns
22
+ * the slot from the file alone. All functions here are pure string transforms —
23
+ * file I/O and durability live in the plugin's apply.
24
+ *
25
+ * @module dsh-loop-engine/patch-manager
26
+ */
27
+ import type { LoopEngineId } from './settings.ts';
28
+ /** Begin marker of the plugin-managed span inside a profile patch file. */
29
+ export declare const MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
30
+ /** End marker of the plugin-managed span inside a profile patch file. */
31
+ export declare const MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
32
+ /** Render the managed block for one engine; `in-process` returns the empty span. */
33
+ export declare function renderManagedBlock(engine: LoopEngineId): string;
34
+ /** Whether a patch-file text contains the managed block span. */
35
+ export declare function hasManagedBlock(text: string): boolean;
36
+ /** Derive the current engine from a patch-file text by the managed block's begin marker. */
37
+ export declare function currentEngineOf(text: string): LoopEngineId;
38
+ /**
39
+ * Produce the next patch-file text for a target engine, preserving every byte
40
+ * outside the managed span. Appends the span when absent; replaces or removes
41
+ * it when present.
42
+ * @param text - current patch-file text.
43
+ * @param engine - target engine.
44
+ * @returns the rewritten patch-file text.
45
+ */
46
+ export declare function applyManagedBlock(text: string, engine: LoopEngineId): string;
47
+ //# sourceMappingURL=patch-manager.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared loop-engine identity, namespace, and schema.
3
+ *
4
+ * The namespace literal lives in the zero-import `./namespace.ts` so both
5
+ * halves agree on the section name: the node half brands it through
6
+ * `settingsNamespace()` (a runtime value), while the browser half imports the
7
+ * same literal without pulling the host-side `dsh-settings` service into the
8
+ * client bundle (cross-plugin value imports go through cordis services, and
9
+ * `settings-scope.ts` follows the same discipline).
10
+ *
11
+ * @module dsh-loop-engine/settings
12
+ */
13
+ import z from '@deepseek-ai/schemastery';
14
+ import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
15
+ export { LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL } from './namespace.ts';
16
+ /** The installed engine driving new Agent turns. */
17
+ export declare const LOOP_ENGINE_IDS: readonly ["in-process", "claude-code", "codex", "pi"];
18
+ /** Installed agent loop engine id. */
19
+ export type LoopEngineId = (typeof LOOP_ENGINE_IDS)[number];
20
+ /** Stored and composed loop engine selection. */
21
+ export interface LoopEngineSettings {
22
+ /** The engine future Agents are created on. */
23
+ engine: LoopEngineId;
24
+ }
25
+ /** Schema of the loop engine settings section. */
26
+ export declare const LOOP_ENGINE_SETTINGS_SCHEMA: z<LoopEngineSettings>;
27
+ /** Brand the shared literal through the settings API on the node side. */
28
+ export declare function loopEngineSettingsNamespace(): SettingsNamespace;
29
+ //# sourceMappingURL=settings.d.ts.map