@vidge/dsh-agent-hub 0.1.0-rc1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/README.zh.md +115 -0
  4. package/cordis.patch.yml +22 -0
  5. package/lib/client.js +1494 -0
  6. package/lib/index.js +5045 -0
  7. package/lib/invariant.js +96 -0
  8. package/lib/types/client/LoopEngineComposerSelect.d.ts +73 -0
  9. package/lib/types/client/LoopEngineSection.d.ts +43 -0
  10. package/lib/types/client/engine-rpc.d.ts +74 -0
  11. package/lib/types/client/index.d.ts +28 -0
  12. package/lib/types/client/locales.d.ts +44 -0
  13. package/lib/types/client/session-location.d.ts +63 -0
  14. package/lib/types/client/store.d.ts +58 -0
  15. package/lib/types/commands.d.ts +69 -0
  16. package/lib/types/driver-core/context-files.d.ts +62 -0
  17. package/lib/types/driver-core/ownership.d.ts +40 -0
  18. package/lib/types/driver-core/permission-knobs.d.ts +26 -0
  19. package/lib/types/driver-core/prompt.d.ts +23 -0
  20. package/lib/types/driver-core/skill-inject.d.ts +59 -0
  21. package/lib/types/engine-claude/agent.d.ts +116 -0
  22. package/lib/types/engine-claude/loop.d.ts +99 -0
  23. package/lib/types/engine-claude/mapping.d.ts +84 -0
  24. package/lib/types/engine-claude/permission.d.ts +41 -0
  25. package/lib/types/engine-claude/process.d.ts +59 -0
  26. package/lib/types/engine-claude/provider-env.d.ts +50 -0
  27. package/lib/types/engine-claude/sdk.d.ts +101 -0
  28. package/lib/types/engine-claude/types.d.ts +28 -0
  29. package/lib/types/engine-codex/agent.d.ts +109 -0
  30. package/lib/types/engine-codex/appserver/client.d.ts +49 -0
  31. package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
  32. package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
  33. package/lib/types/engine-codex/appserver/types.d.ts +215 -0
  34. package/lib/types/engine-codex/loop.d.ts +92 -0
  35. package/lib/types/engine-codex/permission.d.ts +32 -0
  36. package/lib/types/engine-codex/skills.d.ts +29 -0
  37. package/lib/types/engine-codex/types.d.ts +19 -0
  38. package/lib/types/engine-pi/agent.d.ts +125 -0
  39. package/lib/types/engine-pi/loop.d.ts +96 -0
  40. package/lib/types/engine-pi/permission.d.ts +43 -0
  41. package/lib/types/engine-pi/rpc/client.d.ts +105 -0
  42. package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
  43. package/lib/types/engine-pi/rpc/types.d.ts +235 -0
  44. package/lib/types/engine-pi/skills.d.ts +55 -0
  45. package/lib/types/engine-pi/types.d.ts +27 -0
  46. package/lib/types/engine-record.d.ts +124 -0
  47. package/lib/types/index.d.ts +138 -0
  48. package/lib/types/invariant.d.ts +23 -0
  49. package/lib/types/llm-compat.d.ts +32 -0
  50. package/lib/types/namespace.d.ts +19 -0
  51. package/lib/types/patch-manager.d.ts +78 -0
  52. package/lib/types/router.d.ts +189 -0
  53. package/lib/types/rpc.d.ts +113 -0
  54. package/lib/types/settings.d.ts +29 -0
  55. package/lib/types/skills.d.ts +93 -0
  56. package/package.json +107 -0
@@ -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-agent-hub/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,55 @@
1
+ /**
2
+ * Pi skill provider: exposes the Pi CLI's instruction files and skills as DSH
3
+ * skills.
4
+ *
5
+ * Pi reads per-directory context files (`AGENTS.md`, or `CLAUDE.md`,
6
+ * preferring `AGENTS.override.md` where one exists) from the session cwd up to
7
+ * the git root, plus a global `AGENTS.md` under the pi config directory
8
+ * (`PI_CODING_AGENT_DIR` or `~/.pi/agent`), and installs skills from
9
+ * `skills/` directories (`~/.pi/agent/skills/` and project `.pi/skills/`
10
+ * walking up). Each context-file set is surfaced as one user-invocable
11
+ * `agents-md` skill whose body is the concatenated file contents; every found
12
+ * `SKILL.md` catalog entry is surfaced under its own name, so the dsh
13
+ * skill-injection seam (`/name` gestures) can carry them into the prompt.
14
+ *
15
+ * `.agents/skills` roots are deliberately not scanned here: dsh's own
16
+ * `skill-filesystem` provider already exposes them through the same registry
17
+ * in the web profile. Pi settings/CLI/package skills are only discoverable
18
+ * through a running `pi --mode rpc` probe, which the engine does not perform
19
+ * at composition time — the filesystem subset above is authoritative for the
20
+ * web menu.
21
+ *
22
+ * @module dsh-agent-hub/engine-pi/skills
23
+ */
24
+ import type { SkillCandidate, SkillDefinition, SkillLookupOptions, SkillProvider, SkillProviderControl } from '../skills.ts';
25
+ /**
26
+ * Resolve the pi config directory, honoring the `PI_CODING_AGENT_DIR`
27
+ * environment override and falling back to `~/.pi/agent`.
28
+ * @returns the absolute pi config directory.
29
+ */
30
+ export declare function piAgentDir(): string;
31
+ /**
32
+ * Skill provider that discovers context files and skills from pi's standard
33
+ * locations:
34
+ * - project context files between the cwd and the git root (plus
35
+ * `~/.pi/agent/AGENTS.md`) — surfaced as one `agents-md` skill;
36
+ * - project `.pi/skills/` and user `~/.pi/agent/skills/` — each `SKILL.md`
37
+ * entry surfaced under its own name.
38
+ */
39
+ export declare class PiSkillProvider implements SkillProvider {
40
+ private readonly control;
41
+ readonly name = "pi";
42
+ constructor(control: SkillProviderControl);
43
+ list(options: SkillLookupOptions): Promise<readonly SkillCandidate[]>;
44
+ get(candidate: SkillCandidate, _options: SkillLookupOptions): Promise<SkillDefinition | undefined>;
45
+ /** One merged `agents-md` candidate for a ranked file set. */
46
+ private agentsCandidate;
47
+ /** Collect every skill in one skills directory, both pi layouts. */
48
+ private collectSkillsDir;
49
+ /** One parsed skill as a ranked candidate. */
50
+ private skillCandidate;
51
+ /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
52
+ private tryParse;
53
+ }
54
+ export default PiSkillProvider;
55
+ //# 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-agent-hub/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,124 @@
1
+ /**
2
+ * Durable per-session engine records.
3
+ *
4
+ * A session's engine must survive the process, because resuming a session on
5
+ * a different engine replays history that engine cannot act on. The harness
6
+ * offers no writable durable slot for this:
7
+ *
8
+ * - `SessionHeader.agentPreset` is owned by the agent-presets subsystem — the
9
+ * web controller writes it through `composeAgent()` and asserts it unchanged
10
+ * on resume — and `SessionHeader` has no free-form dict (its fields are
11
+ * closed at two `meta` literals and the SQLite column list).
12
+ * - A custom session event is worse than unavailable: the persistence
13
+ * coordinator refuses to load any log carrying a type outside the generated
14
+ * `KNOWN_SESSION_EVENT_TYPES` unless the envelope sets `ignorable`, which
15
+ * `Session.append()` provides no way to do. An out-of-repo event would make
16
+ * every session this plugin wrote permanently unloadable.
17
+ *
18
+ * So the record lives in a plugin-owned sidecar keyed by session id, written
19
+ * with the same atomic temp+rename discipline as the managed patch file.
20
+ *
21
+ * @module dsh-agent-hub/engine-record
22
+ */
23
+ import { type LoopEngineId } from './settings.ts';
24
+ /** Backend-owned artifact location, as returned by `sessionPersistence.locate`. */
25
+ export interface SessionLocation {
26
+ readonly kind: string;
27
+ readonly path: string;
28
+ }
29
+ /**
30
+ * The session metadata `locate` reads to derive an artifact path.
31
+ *
32
+ * `cwd` is NOT optional decoration: the JSONL backend groups sessions into a
33
+ * per-project directory keyed by it (`projectDir(root, cwd)`), and an
34
+ * `undefined` cwd selects the literal `_no-cwd` bucket. Passing only `id`
35
+ * therefore returns a path in a directory the session does not live in — the
36
+ * sidecar is written where nothing will ever read it, and every resume falls
37
+ * through to the `in-process` default.
38
+ */
39
+ export interface SessionMetaLike {
40
+ readonly id: string;
41
+ readonly cwd?: string;
42
+ }
43
+ /**
44
+ * The persistence surface the record store borrows. Declared structurally so
45
+ * this module needs no peer dependency on the persistence package, and so a
46
+ * profile without persistence degrades to the shared fallback directory.
47
+ */
48
+ export interface RecordPersistence {
49
+ locate(meta: SessionMetaLike): SessionLocation | undefined;
50
+ inspect(id: string, signal?: AbortSignal): Promise<{
51
+ meta: SessionMetaLike;
52
+ }>;
53
+ }
54
+ /** Fallback directory for backends with no per-session artifact (e.g. SQLite). */
55
+ export declare function fallbackDir(): string;
56
+ /** The fallback record path for one session id. */
57
+ export declare function fallbackPathFor(sessionId: string): string;
58
+ /**
59
+ * Reads and writes the per-session engine record.
60
+ *
61
+ * The store prefers a sidecar beside the backend's own per-session artifact so
62
+ * the record travels with the session; backends that own no such artifact fall
63
+ * back to a shared directory under the dsh home.
64
+ */
65
+ export declare class EngineRecordStore {
66
+ private readonly persistence;
67
+ constructor(persistence: () => RecordPersistence | undefined);
68
+ /**
69
+ * Resolve the record path for a session, preferring the backend's artifact
70
+ * directory. Uses `locate`, which explicitly does not read, create, or
71
+ * materialize the artifact.
72
+ *
73
+ * `cwd` must be supplied whenever it is known: it selects the backend's
74
+ * per-project directory, so omitting it silently addresses a different
75
+ * directory than the one holding the session.
76
+ */
77
+ private pathFor;
78
+ /**
79
+ * Record a session's engine. Called before the engine creates the session,
80
+ * so the record is never missing for a session that exists.
81
+ * @param meta - the session being created, including the `cwd` that keys its artifact directory.
82
+ * @param engine - the engine that will own it.
83
+ */
84
+ remember(meta: SessionMetaLike, engine: LoopEngineId): Promise<void>;
85
+ /**
86
+ * Recover a session's engine.
87
+ *
88
+ * The session's `cwd` is not known to the caller on the resume path — only
89
+ * its id — so the backend's own header is consulted first to rebuild the
90
+ * artifact path. A backend that cannot inspect (or a session it does not
91
+ * know) degrades to the shared fallback directory rather than failing the
92
+ * resume: a lost record costs the default engine, an exception costs the
93
+ * session.
94
+ *
95
+ * Sessions written before this plugin gained per-session routing carry no
96
+ * record; they ran on whichever engine was globally selected, and the base
97
+ * in-process loop is the only safe default — it is what an unpatched profile
98
+ * boots with.
99
+ * @param sessionId - the session being resumed.
100
+ * @returns the recorded engine, or `in-process` when no record exists.
101
+ */
102
+ recall(sessionId: string): Promise<LoopEngineId>;
103
+ /**
104
+ * Rebuild a session's sidecar path from the backend's stored header.
105
+ *
106
+ * `inspect` is the correct probe here rather than `prepare`: it is a
107
+ * non-exclusive borrow that returns a validated header and leaves the
108
+ * coordinator's reusable cold session in place for the real engine's
109
+ * subsequent `prepare`, whereas `prepare` would take the exclusive
110
+ * reservation out from under it.
111
+ *
112
+ * @param sessionId - the session whose artifact directory is wanted.
113
+ * @returns the sidecar path, or `undefined` when it cannot be derived.
114
+ */
115
+ private locateByInspect;
116
+ /**
117
+ * Drop fallback records whose session no longer resolves, bounded per run so
118
+ * a large history cannot stall startup. Sidecars beside a backend artifact
119
+ * need no sweep: the backend removes the directory with the session.
120
+ * @param limit - maximum number of records to examine.
121
+ */
122
+ collectOrphans(limit?: number): Promise<number>;
123
+ }
124
+ //# sourceMappingURL=engine-record.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Web-switchable agent loop engine, node half.
3
+ *
4
+ * Hosts every agent-loop engine (the base in-process loop, Claude Code, Codex,
5
+ * and Pi) and gives each session its own. The harness admits exactly one
6
+ * AgentFactory, so this plugin registers a {@link LoopEngineRouter} in that slot
7
+ * for the life of the process and mounts the four engines behind it through
8
+ * shadowed contexts that redirect their `setFactory` calls into router
9
+ * registration. The engine classes themselves are unmodified.
10
+ *
11
+ * A session's engine is fixed when it is created and recorded durably
12
+ * ({@link EngineRecordStore}), because resuming a session on a different engine
13
+ * would replay history that engine cannot act on — each engine writes its own
14
+ * provenance. The `agent-loop-engine` settings section therefore selects the
15
+ * engine for the *next new* session only; switching it never disturbs a running
16
+ * session and needs no restart.
17
+ *
18
+ * The managed block in the profile's `cordis.patch.yml` is now permanent and
19
+ * engine-independent: it disables the base bundle's `agent-loop` row so this
20
+ * plugin owns the slot in every configuration.
21
+ *
22
+ * @module dsh-agent-hub
23
+ */
24
+ import { Context, type Fiber } from '@deepseek-ai/cordis';
25
+ import z from '@deepseek-ai/schemastery';
26
+ import { type Config as ClaudeCodeConfig } from './engine-claude/loop.ts';
27
+ import type { CodexApprovalPolicy, CodexSandboxMode } from './engine-codex/types.ts';
28
+ import { type LoopEngineId } from './settings.ts';
29
+ export declare const name = "loop-engine";
30
+ /**
31
+ * Services the plugin's own fiber requires.
32
+ *
33
+ * `agents` and `systemPrompt` are read directly by {@link apply}: the router
34
+ * claims the single AgentFactory slot on `ctx.agents`, and each engine mounts
35
+ * through a context that shadows both members. Cordis refuses a bare property
36
+ * read on an undeclared service ("cannot get property \"agents\" without
37
+ * inject"), so both must be listed — and listing them also makes the fiber wait
38
+ * for them rather than racing their providers.
39
+ *
40
+ * The optional host services (`commands`, `skills`) stay out: they are resolved
41
+ * lazily via `ctx.get` and may legitimately be absent from a minimal profile.
42
+ * The hosted engine factories declare their own `inject` when mounted as
43
+ * children.
44
+ */
45
+ export declare const inject: string[];
46
+ /** Composition entry for the loop engine selection and the hosted engine drivers. */
47
+ export interface Config extends ClaudeCodeConfig {
48
+ /** Profile whose `cordis.patch.yml` carries the managed block; defaults to `web`. */
49
+ profile?: string;
50
+ /** Patch file name inside the profile; defaults to `cordis.patch.yml`. */
51
+ patchFilename?: string;
52
+ /** Explicit absolute path to the patch file, overriding profile + filename. */
53
+ patchPath?: string;
54
+ /** Pinned Codex sandbox mode; falls back to the session's dsh permission knobs. */
55
+ sandboxMode?: CodexSandboxMode;
56
+ /** Pinned Codex approval policy; falls back to the session's dsh permission knobs. */
57
+ approvalPolicy?: CodexApprovalPolicy;
58
+ /** LLM provider for the Pi RPC child (`--provider`). */
59
+ piProvider?: string;
60
+ /** Thinking/reasoning level for the Pi RPC child, appended to its `--model`. */
61
+ piThinking?: string;
62
+ }
63
+ /**
64
+ * Schema of the loop engine composition entry.
65
+ *
66
+ * A schemastery object validates each field only when it is present and lets
67
+ * an absent key fall through as `undefined`, so omitted knobs are accepted —
68
+ * matching the permissive interface and read path (`resolvePatchPath` defaults
69
+ * the patch path; each engine driver resolves only the knobs it owns and
70
+ * omitted deployment tunables fall back to the session). The composition entry
71
+ * is an engine-agnostic superset: the selectable knobs belong to whichever
72
+ * engine the settings pick at runtime, so both engines' knobs may coexist and
73
+ * only the selected one is consumed.
74
+ */
75
+ export declare const Config: z<Config>;
76
+ /** Resolve the managed patch file from configuration, defaulting to the web profile. */
77
+ export declare function resolvePatchPath(config: Config): string;
78
+ /** Atomically replace the patch file (same-directory temp + rename). */
79
+ export declare function writePatchFile(path: string, text: string): Promise<void>;
80
+ /**
81
+ * Synchronously atomically replace the patch file. The engine-selection
82
+ * onChange is a synchronous hook with no await, and the write MUST land before
83
+ * the caller is told the switch committed — otherwise a user who restarts
84
+ * `dsh web` immediately reads the stale file and the previous engine boots.
85
+ * @param path - the profile's patch file.
86
+ * @param text - the next file content.
87
+ */
88
+ export declare function writePatchFileSync(path: string, text: string): void;
89
+ /**
90
+ * Ensure the permanent managed block is present, preserving the rest of the
91
+ * file byte for byte. Only writes when the file actually differs.
92
+ * @param path - the profile's patch file.
93
+ * @returns whether a write occurred.
94
+ */
95
+ export declare function syncManagedBlock(path: string): Promise<boolean>;
96
+ /**
97
+ * Mount the base in-process loop behind the router.
98
+ *
99
+ * The plugin now owns the AgentFactory slot in every configuration, so the base
100
+ * loop can no longer register itself through the bundle — the managed block
101
+ * disables its row. It is instead hosted here, through the same shadowed
102
+ * context as the other engines, which keeps `in-process` a first-class
103
+ * per-session choice.
104
+ *
105
+ * The import is dynamic and failure-tolerant: the package is a peer, and a
106
+ * deployment that omits it should lose only the in-process engine rather than
107
+ * failing the whole plugin tree.
108
+ *
109
+ * @param ctx - the plugin context, used for diagnostics.
110
+ * @param loopCtx - the shadowed context that redirects `setFactory` to the router.
111
+ * @param mount - the shared fiber-start helper.
112
+ */
113
+ export declare function mountBaseLoop(ctx: Context, loopCtx: Context, mount: (engine: LoopEngineId, plugin: () => (Fiber & PromiseLike<Fiber>)) => void): void;
114
+ /**
115
+ * Register an engine's skill provider into ONE agent's own scope layer.
116
+ *
117
+ * The harness's skill registry is layered by `scopeOf(ctx)`, and every engine
118
+ * mints its agent as its own scope key then hands that scope-tagged context to
119
+ * `setup`. Registering through that context therefore makes the provider
120
+ * visible to exactly one session — a codex session never sees
121
+ * `~/.claude/skills/` — with no per-provider engine predicate, and the
122
+ * registration unwinds with the agent's scope.
123
+ *
124
+ * `in-process` contributes nothing: the base loop brings the harness's own
125
+ * skills, and this plugin adds no engine-specific ones for it.
126
+ *
127
+ * @param engine - the engine that owns the session being set up.
128
+ * @param agentCtx - the scope-tagged agent context the engine passes to `setup`.
129
+ */
130
+ export declare function registerEngineSkills(engine: LoopEngineId, agentCtx: Context): void;
131
+ /**
132
+ * Apply the plugin: own the AgentFactory slot with the router, mount every
133
+ * engine behind it, and track the selection for new sessions.
134
+ * @param ctx - the composing context.
135
+ * @param config - composition entry for the managed patch file.
136
+ */
137
+ export declare function apply(ctx: Context, config: Config): void;
138
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Package-owned invariant companion for the managed patch block.
3
+ *
4
+ * The plugin's owned relationship is that the managed block is a permanent
5
+ * fixed point: applying it is idempotent, it always yields the row that frees
6
+ * the AgentFactory slot for this plugin's router, and it upgrades a legacy
7
+ * engine-tagged block from the era when the block encoded the selection. The
8
+ * companion asserts these against the pure transform.
9
+ *
10
+ * @module dsh-agent-hub/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,32 @@
1
+ /**
2
+ * Cross-version shim for the one dsh-llm export that was renamed mid-line.
3
+ *
4
+ * `dsh-llm` renamed its tool-call brand between the two published lines:
5
+ *
6
+ * - `0.1.1-rc.2` (dist-tag `latest`, what plain `dsh` installs) exports `CallId`
7
+ * - `0.1.2-alpha.2` (dist-tag `alpha`, the harness monorepo source) exports `ToolCallId`
8
+ *
9
+ * A named import of either one is a hard ESM link error against the other line
10
+ * ("does not provide an export named ..."), which fails the whole plugin tree at
11
+ * boot before any of our code runs. A namespace import is not checked per-member
12
+ * at link time, so it loads under both and we can pick the survivor at runtime.
13
+ *
14
+ * The brand type is deliberately derived from `ToolResultMessageInput['callId']`
15
+ * rather than hard-coded to either `Branded<'CallId'>` or `Branded<'ToolCallId'>`:
16
+ * the two lines brand with different literals, so only the installed package's
17
+ * own view of the type is assignable to the APIs we hand these values to.
18
+ *
19
+ * @module dsh-agent-hub/llm-compat
20
+ */
21
+ import type { ToolResultMessageInput } from '@deepseek-ai/dsh-llm';
22
+ /**
23
+ * The tool-call id brand, as the *installed* dsh-llm defines it. Aliased off a
24
+ * consuming API so it stays correct on both the `CallId` and `ToolCallId` lines.
25
+ */
26
+ export type CallId = ToolResultMessageInput['callId'];
27
+ /**
28
+ * Brand a raw string as a tool-call id, resolving to whichever name the
29
+ * installed dsh-llm publishes.
30
+ */
31
+ export declare const CallId: (id: string) => CallId;
32
+ //# sourceMappingURL=llm-compat.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Loop engine constants shared by both halves, in a module with no runtime
3
+ * imports so the browser bundle can import them without dragging
4
+ * `dsh-settings` (a host-side service) into the client artifact.
5
+ *
6
+ * That constraint is load-bearing, not stylistic: the browser bundle inlines
7
+ * non-seed specifiers (see `build.mjs`), so a value imported from
8
+ * `./settings.ts` — which imports `dsh-settings` — pulls the whole host-side
9
+ * settings module into the client build. Anything both halves need as a
10
+ * *value* belongs here; `./settings.ts` re-exports it for the node half.
11
+ * @module dsh-agent-hub/namespace
12
+ */
13
+ /** Settings namespace carrying the deployment's selected agent loop engine. */
14
+ export declare const LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
15
+ /** The installed engines driving new Agent turns. */
16
+ export declare const LOOP_ENGINE_IDS: readonly ["in-process", "claude-code", "codex", "pi"];
17
+ /** Installed agent loop engine id. */
18
+ export type LoopEngineId = (typeof LOOP_ENGINE_IDS)[number];
19
+ //# sourceMappingURL=namespace.d.ts.map