dsh-loop-engine 0.1.5-rc2 → 0.1.5-rc4

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 (54) hide show
  1. package/README.md +44 -177
  2. package/README.zh.md +48 -100
  3. package/lib/client.js +887 -266
  4. package/lib/index.js +2212 -914
  5. package/lib/invariant.js +43 -45
  6. package/lib/types/agent-preset-ids.d.ts +303 -0
  7. package/lib/types/client/LoopEngineBadge.d.ts +44 -17
  8. package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
  9. package/lib/types/client/LoopEngineSection.d.ts +5 -4
  10. package/lib/types/client/locales.d.ts +133 -7
  11. package/lib/types/client/reload.d.ts +135 -0
  12. package/lib/types/client/session-engine.d.ts +474 -0
  13. package/lib/types/client/store.d.ts +1 -1
  14. package/lib/types/client/turn-status.d.ts +112 -10
  15. package/lib/types/client/use-session-engine.d.ts +66 -0
  16. package/lib/types/commands.d.ts +11 -3
  17. package/lib/types/driver-core/host-servers.d.ts +106 -0
  18. package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
  19. package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
  20. package/lib/types/driver-core/model-handover.d.ts +116 -0
  21. package/lib/types/driver-core/ownership.d.ts +6 -5
  22. package/lib/types/driver-core/prompt.d.ts +32 -0
  23. package/lib/types/driver-core/session-lifetime.d.ts +62 -0
  24. package/lib/types/driver-core/session-model.d.ts +82 -0
  25. package/lib/types/engine-claude/agent.d.ts +23 -3
  26. package/lib/types/engine-claude/loop.d.ts +16 -15
  27. package/lib/types/engine-codex/agent.d.ts +22 -3
  28. package/lib/types/engine-codex/appserver/client.d.ts +15 -2
  29. package/lib/types/engine-codex/loop.d.ts +13 -15
  30. package/lib/types/engine-codex/model-handover.d.ts +44 -0
  31. package/lib/types/engine-kimi/acp/client.d.ts +10 -0
  32. package/lib/types/engine-kimi/agent.d.ts +19 -2
  33. package/lib/types/engine-kimi/commands.d.ts +18 -14
  34. package/lib/types/engine-kimi/loop.d.ts +14 -16
  35. package/lib/types/engine-kimi/model-handover.d.ts +32 -0
  36. package/lib/types/engine-kimi/process.d.ts +2 -2
  37. package/lib/types/engine-kimi/types.d.ts +1 -1
  38. package/lib/types/engine-of-session.d.ts +97 -0
  39. package/lib/types/engine-pi/agent.d.ts +25 -23
  40. package/lib/types/engine-pi/loop.d.ts +13 -23
  41. package/lib/types/engine-pi/model-handover.d.ts +35 -0
  42. package/lib/types/engine-pi/types.d.ts +2 -2
  43. package/lib/types/engine-remote.d.ts +192 -0
  44. package/lib/types/engine-surface.d.ts +36 -0
  45. package/lib/types/index.d.ts +51 -50
  46. package/lib/types/invariant.d.ts +8 -5
  47. package/lib/types/model-selection-reset.d.ts +271 -0
  48. package/lib/types/patch-manager.d.ts +57 -39
  49. package/lib/types/preset.d.ts +39 -26
  50. package/lib/types/provider-route.d.ts +83 -36
  51. package/lib/types/router-loop.d.ts +406 -0
  52. package/lib/types/session-engine-store.d.ts +138 -0
  53. package/lib/types/settings.d.ts +12 -11
  54. package/package.json +109 -104
@@ -48,8 +48,8 @@ export declare function kimiBinResolver(configBin?: string): string;
48
48
  /**
49
49
  * Build the persistent `kimi acp` argv. The ACP child stays alive across steps
50
50
  * and is spoken to over JSON-RPC on stdio — the prompt is a request body, not an
51
- * argv positional — so there is no command-line length ceiling and no model flag
52
- * (Kimi owns model selection natively via its own config).
51
+ * argv positional — so there is no command-line length ceiling and no model flag:
52
+ * a model is selected per ACP session through `session/set_model`, not argv.
53
53
  * @param bin - the Kimi executable.
54
54
  * @returns the argv, `argv[0]` being the executable.
55
55
  */
@@ -13,7 +13,7 @@
13
13
  */
14
14
  /** Driver configuration after defaults and load-time validation. */
15
15
  export interface ResolvedConfig {
16
- /** Model alias the `kimi` child is launched with (`--model`); Kimi native config owns the model when omitted. */
16
+ /** Model sent per ACP session (`session/set_model`) when the session selects none; Kimi native config owns the model when omitted. */
17
17
  readonly model: string | undefined;
18
18
  /** Explicit environment entries layered over the credential-scrubbed parent environment. */
19
19
  readonly env: Record<string, string>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The engine one session runs: the single authoritative read, shared by the
3
+ * router's routing decision and the plugin's own Remote.
4
+ *
5
+ * Two facts can answer it, in this order:
6
+ *
7
+ * 1. THE PLUGIN'S OWN RECORD — `$DSH_HOME/.loop-engine/engines.json` (see
8
+ * `./session-engine-store.ts`). It exists because a user may move a session
9
+ * to another engine at any time, including long after the session started,
10
+ * and the harness's preset channel refuses exactly that
11
+ * (`agent-preset/locked`), so the plugin keeps the choice itself. When the
12
+ * record names an engine, that IS the session's engine and nothing else is
13
+ * consulted — not even the durable log, which is why a session with a record
14
+ * still reads correctly when its persistence is unavailable.
15
+ * 2. THE RECORDED AGENT PRESET, for every session the plugin has no record
16
+ * for: a session created before per-session switching existed, one created
17
+ * on a preset this plugin does not own, or one in a deployment whose record
18
+ * file is gone. That answer comes from the durable log — the harness's
19
+ * `agentPreset` projection folds the session header together with every
20
+ * committed `agent-preset/selected`, so reading it IS reading the log — and
21
+ * it never comes from the session header alone (a creation fact, deep-frozen)
22
+ * or from a client-side listing hint (a partial cache).
23
+ *
24
+ * This module is the ONLY read: the router routes on what it returns and the
25
+ * plugin's own Remote reports the same value, so the engine a session is driven
26
+ * by and the engine it is shown as running cannot disagree.
27
+ *
28
+ * There is one refinement on top of it, and it is the whole reason
29
+ * {@link engineReportOfSession} exists: a session with a LIVE agent has a better
30
+ * answer than its record — the agent in front of it — and the two can differ
31
+ * (only when a release of that agent did not take, since every other switch
32
+ * either swaps the agent in place or leaves the session cold). The report
33
+ * carries both, never blending them (see {@link SessionEngineReport}).
34
+ *
35
+ * The secondary read goes through the same seam the host itself uses before
36
+ * choosing the composition to mount
37
+ * (`ctx.sessionQuery.observeSession(id, { projectionMode: 'all' })`, see
38
+ * `packages/api/session-controller/src/agent.ts` in the harness). The
39
+ * observation is a read-only lease: it is released immediately through `using`,
40
+ * takes no write handle, and holds no write lock.
41
+ *
42
+ * @module dsh-loop-engine/engine-of-session
43
+ */
44
+ import type { Context } from '@deepseek-ai/cordis';
45
+ import type { SessionId } from '@deepseek-ai/dsh-session';
46
+ import { type LoopEngineId, type SessionEngine, type SessionEngineReport } from './agent-preset-ids.ts';
47
+ import type { EngineRecordSource } from './session-engine-store.ts';
48
+ /**
49
+ * The engine one session runs, read from the plugin's own record first and from
50
+ * the durable `agentPreset` projection otherwise.
51
+ *
52
+ * A deployment that composes no `sessionQuery` service has no durable log to
53
+ * ask, so a session with no record reads as {@link SessionEngine} `unset`: no
54
+ * engine is claimed for it, and the router falls back exactly as it does for a
55
+ * session that recorded nothing.
56
+ * @param ctx - the context carrying the session-query seam.
57
+ * @param sessionId - the persisted session to inspect.
58
+ * @param records - the plugin's own per-session engine record.
59
+ * @returns what the plugin's record, or else the durable log, says about that
60
+ * session's engine.
61
+ */
62
+ export declare function engineOfSession(ctx: Context, sessionId: SessionId, records: EngineRecordSource): Promise<SessionEngine>;
63
+ /**
64
+ * Reports the engine of one session's LIVE agent, when this process has one:
65
+ * the loop router's own bookkeeping (`RouterLoop.live`), which is the only
66
+ * record of what it actually built for a session.
67
+ *
68
+ * A reader rather than the router itself, so this fold can be exercised without
69
+ * one — and so the router stays the only thing that owns its bookkeeping.
70
+ */
71
+ export type LiveEngineReader = (sessionId: SessionId) => LoopEngineId | undefined;
72
+ /**
73
+ * What one session actually runs, and — when they differ — the engine its own
74
+ * record names.
75
+ *
76
+ * The live agent outranks the record, because it is the answer to the question
77
+ * this report exists to answer honestly: what is driving this session NOW. The
78
+ * record is still consulted, but as the OTHER fact — the engine this session
79
+ * takes when it is next built. A record that equals the live agent is no news
80
+ * and travels as absent; a live agent with no record at all has nothing to
81
+ * report beside it, because a session with no record was never switched.
82
+ *
83
+ * A session with NO live agent answers exactly as {@link engineOfSession} does:
84
+ * nothing is running, so the record (or, with no record, the recorded preset) IS
85
+ * the engine that session runs — the one its next build will use, which is the
86
+ * state a switch that released the session's agent leaves it in. There is no
87
+ * second fact to report and never a pending one.
88
+ * @param ctx - the context carrying the session-query seam.
89
+ * @param sessionId - the session to report on.
90
+ * @param records - the plugin's own per-session engine record.
91
+ * @param liveEngineOf - the engine of this session's live agent, when this
92
+ * process has one (the router's bookkeeping).
93
+ * @returns the engine driving the session, plus the recorded engine it is not
94
+ * running when the two differ.
95
+ */
96
+ export declare function engineReportOfSession(ctx: Context, sessionId: SessionId, records: EngineRecordSource, liveEngineOf: LiveEngineReader): Promise<SessionEngineReport>;
97
+ //# sourceMappingURL=engine-of-session.d.ts.map
@@ -15,11 +15,15 @@ import type { Scope } from '@deepseek-ai/dsh-scope';
15
15
  import type { Session, SessionId, UserMessage } from '@deepseek-ai/dsh-session';
16
16
  import type { Context } from '@deepseek-ai/cordis';
17
17
  import type { ResolvedConfig } from './types.ts';
18
- import type { PiModelEntry } from './probe.ts';
19
18
  import { DriverInbox } from '../driver-core/inbox.ts';
20
19
  import { type PiSpawnCapability } from './rpc/client.ts';
21
- /** Provider route label used for logged header snapshots and message provenance. */
22
- export declare const PROVIDER = "pi";
20
+ /**
21
+ * Provider route label this driver logs into request/header snapshots and
22
+ * message provenance — the ONE route every hosted engine shares
23
+ * ({@link HOSTED_ROUTE_LABEL}), so all four engines select `external/default`
24
+ * and the model menu carries a single group instead of one per engine.
25
+ */
26
+ export declare const PROVIDER = "external";
23
27
  /** Drives one session through turn and step boundaries on Pi. */
24
28
  export declare class PiAgent implements Agent {
25
29
  private loopCtx;
@@ -29,7 +33,6 @@ export declare class PiAgent implements Agent {
29
33
  private readonly config;
30
34
  private readonly spawn;
31
35
  private readonly bin;
32
- private readonly catalog;
33
36
  readonly inbox: DriverInbox;
34
37
  private phase;
35
38
  private activityDone;
@@ -61,9 +64,7 @@ export declare class PiAgent implements Agent {
61
64
  * @param phase - the running phase carrying the open step's position.
62
65
  */
63
66
  private beginSegment;
64
- constructor(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session, config: ResolvedConfig, spawn: PiSpawnCapability, bin: string, catalog: {
65
- readonly entries: readonly PiModelEntry[];
66
- });
67
+ constructor(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session, config: ResolvedConfig, spawn: PiSpawnCapability, bin: string);
67
68
  /**
68
69
  * Open this step's RPC child. The Pi RPC process is single-session, so a step
69
70
  * never reuses one: the step teardown disposes it and the next step respawns
@@ -135,23 +136,24 @@ export declare class PiAgent implements Agent {
135
136
  /** Append the request header snapshot once per loop instance. */
136
137
  private assertRequestHeader;
137
138
  /**
138
- * The harness Session's web-side model selection, if any was stored. The
139
- * durable `model/selection` event carries `{ provider, model, ... }`; when a
140
- * user picked a model via `/model`, this is the newest pick, and it overrides
141
- * the deployment config (which stays the fallback). Returns `undefined` when
142
- * no selection was stored, so the deployment config governs.
143
- */
144
- private dynamicModel;
145
- /** Build the `pi --mode rpc` argv/cwd/env for one step's child process. */
146
- /**
147
- * Resolve the `--model` for the RPC child. The session-selected model (last
148
- * `model/selection` event) is honored only when it is one of pi's discovered
149
- * models; an unknown harness model (e.g. another provider's model such as
150
- * `anyai-v1`, which pi cannot serve) is dropped so the child falls back to pi's
151
- * own default instead of exiting with "Model ... not found". An empty catalog
152
- * (probe not concluded) keeps the candidate, matching prior behavior.
139
+ * Build the `pi --mode rpc` argv/cwd/env for one step's child process.
140
+ *
141
+ * The model comes from the session's own selection when it names a real dsh
142
+ * model, else from the deployment's pinned configuration
143
+ * ({@link sessionModelOverrideOf}). Pi's `--model` flag is
144
+ * `"provider/id"`-qualified, so a session pick travels as the composite —
145
+ * which also carries the provider, leaving the deployment's `--provider` out
146
+ * of the argv for that step. A step with neither a selection nor a pin sends
147
+ * no `--model` at all and lets Pi's own configuration decide. Read every step,
148
+ * so a model picked mid-conversation lands on the next child.
149
+ *
150
+ * When dsh also discloses that model's ENDPOINT, the child is pointed at it:
151
+ * `--provider` names the provider pi's `models.json` declares, `--api-key`
152
+ * carries the credential, and `PI_CODING_AGENT_DIR` redirects pi's agent
153
+ * directory to the plugin-owned one holding that `models.json`
154
+ * ({@link piAgentDir}) — so the child runs on dsh's endpoint instead of its
155
+ * own `~/.pi` configuration, which is never touched.
153
156
  */
154
- private pickModel;
155
157
  private spawnSpec;
156
158
  /**
157
159
  * Run one Pi RPC query for the current step and map its event stream into the
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Pi loop engine module: hosts the AgentFactory that drives every session
3
- * through the Pi CLI (`@earendil-works/pi-coding-agent`) over its JSONL RPC
4
- * mode, one stateless session per dsh step, with the durable session log as the
5
- * sole source of model context. dsh-loop-engine constructs this factory when
6
- * the Pi engine is selected; this module is a library, not a Cordis plugin
7
- * entry. Pi has no permission system, so the entire `pi --mode rpc` child is
2
+ * Pi loop engine module: drives every session it is handed through the Pi CLI
3
+ * (`@earendil-works/pi-coding-agent`) over its JSONL RPC mode, one stateless
4
+ * session per dsh step, with the durable session log as the sole source of
5
+ * model context. The router routes a session here on the plugin's own engine
6
+ * record, else its agent preset; this module is a library, not a Cordis plugin entry. Pi has no permission system, so the entire `pi --mode rpc` child is
8
7
  * spawned through the dsh subprocess seam — the only available privilege
9
8
  * boundary — and its `--tools` are pruned to the resolved sandbox stance.
10
9
  *
@@ -15,10 +14,9 @@ import z from '@deepseek-ai/schemastery';
15
14
  import type { AgentOptions } from '@deepseek-ai/dsh-agent';
16
15
  import type { Session, SessionId } from '@deepseek-ai/dsh-session';
17
16
  import { PiAgent } from './agent.ts';
18
- import type { PiModelEntry } from './probe.ts';
19
17
  import type { PiProcess, PiSpawnSpec } from './rpc/client.ts';
20
18
  import type { PiSandboxMode, ResolvedConfig } from './types.ts';
21
- import { HostedLoopFactory } from '../driver-core/hosted-loop-factory.ts';
19
+ import { HostedEngineRuntime } from '../driver-core/hosted-engine-runtime.ts';
22
20
  /** Pi CLI sandbox modes a deployment may pin. */
23
21
  export declare const PI_SANDBOX_MODES: readonly PiSandboxMode[];
24
22
  /** Grace in milliseconds for Pi process-tree termination. */
@@ -35,40 +33,32 @@ export interface Config {
35
33
  sandboxMode?: PiSandboxMode;
36
34
  /** LLM provider for the `pi` child (`--provider`), when the deployment pins one. */
37
35
  provider?: string;
38
- /** Model pattern for the `pi` child (`--model`); Pi native settings own the model when omitted. */
36
+ /** Fallback model for the `pi` child (`--model`), used when the session selects none; Pi native settings own the model when omitted. */
39
37
  model?: string;
40
38
  /** Thinking/reasoning level, appended to the `--model` pattern when pinned. */
41
39
  thinkingLevel?: string;
42
40
  /** Explicit environment entries passed to the `pi` child. */
43
41
  env?: Record<string, string>;
44
- /** Shared Pi model catalog holder; the loop writes its `pi --list-models` probe result here. */
45
- piCatalogHolder?: {
46
- entries: readonly PiModelEntry[];
47
- };
48
42
  }
49
43
  /** Schema of the Pi loop plugin configuration. */
50
44
  export declare const Config: z<Config>;
51
- /** Host-face ctx key for the Pi loop service. */
45
+ /** Host-face ctx key this engine's runtime is registered under. */
46
+ export declare const PI_ENGINE_LABEL = "agentLoopPi";
52
47
  declare module '@deepseek-ai/cordis' {
53
48
  interface Context {
54
49
  agentLoopPi: PiLoop;
55
50
  }
56
51
  }
57
52
  /**
58
- * Concrete AgentFactory and driver service of the Pi loop. Creation and resume
59
- * follow the registry factory contract and the shared publication transaction:
60
- * prepare, run setup, then publish through both registries, announce, and emit
61
- * `agent/session-start`.
53
+ * Creation/resume machinery for the Pi engine. The process-wide AgentFactory
54
+ * slot belongs to the router, which delegates each session to the engine its
55
+ * preset names; this class is that engine's driver, not a plugin.
62
56
  */
63
- export declare class PiLoop extends HostedLoopFactory<ResolvedConfig, PiAgent> {
64
- /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
65
- static inject: string[];
57
+ export declare class PiLoop extends HostedEngineRuntime<ResolvedConfig, PiAgent> {
66
58
  /** Process-tree spawn capability handed to every agent, sandboxed by the subprocess seam. */
67
59
  readonly spawn: (spec: PiSpawnSpec) => PiProcess;
68
60
  /** Resolved Pi CLI entrypoint; `argv[0]` of every Pi RPC child. */
69
61
  readonly bin: string;
70
- /** Discovered Pi model catalog, forwarded to each agent so it can validate the session-selected model against what pi can actually serve. */
71
- private readonly catalog;
72
62
  constructor(ctx: Context, config: Config);
73
63
  /** Construct the Pi RPC driver for one prepared session. */
74
64
  protected buildAgent(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session): PiAgent;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pi's dialect for a dsh endpoint: a self-built agent directory plus the argv
3
+ * flags that point the `pi --mode rpc` child at dsh's model.
4
+ *
5
+ * Pi resolves a model through its own agent directory — `models.json` under
6
+ * `PI_CODING_AGENT_DIR` (or `~/.pi` when unset) — and its custom-provider schema
7
+ * has no `apiKeyEnv`: a provider's credential is either a literal `apiKey` in
8
+ * that file or the CLI's own `--api-key`. There is no `PI_*` variable for a base
9
+ * URL either. So the only automatic way to hand pi a dsh endpoint is to give it
10
+ * an agent directory THIS PLUGIN owns, containing a `models.json` that declares
11
+ * the dsh provider, and to point `PI_CODING_AGENT_DIR` at it.
12
+ *
13
+ * The user's `~/.pi` is deliberately never written or read: the plugin owns a
14
+ * separate directory under the OS temp dir, one per distinct endpoint, created
15
+ * 0700 with a 0600 file because it carries the credential. The cost is real and
16
+ * documented: redirecting the agent directory means the child no longer sees the
17
+ * user's own `~/.pi` skills/auth/theme. That is the trade the deployment makes
18
+ * by selecting a real dsh model on pi.
19
+ *
20
+ * @module dsh-loop-engine/engine-pi/model-handover
21
+ */
22
+ import type { DshModelHandover } from '../driver-core/model-handover.ts';
23
+ /**
24
+ * The agent directory for one handover, materialized on first use.
25
+ *
26
+ * The directory name is a hash of the endpoint facts, so two sessions on the
27
+ * same provider/model share one directory and a changed endpoint gets its own;
28
+ * `models.json` declares exactly the provider pi is told to use (`--provider`)
29
+ * and the one model it is told to run (`--model`), keyed the way pi's own
30
+ * schema reads it (`baseUrl`/`api`/`apiKey`/`models`).
31
+ * @param handover - the resolved dsh endpoint for the session's selection.
32
+ * @returns the absolute path to hand the child as `PI_CODING_AGENT_DIR`.
33
+ */
34
+ export declare function piAgentDir(handover: DshModelHandover): string;
35
+ //# sourceMappingURL=model-handover.d.ts.map
@@ -15,9 +15,9 @@ export type PiSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-acces
15
15
  export interface ResolvedConfig {
16
16
  /** Pinned sandbox mode; `undefined` follows the session's dsh permission knobs per query. */
17
17
  readonly sandboxMode: PiSandboxMode | undefined;
18
- /** LLM provider the `pi` RPC process is launched with (`--provider`). */
18
+ /** LLM provider the `pi` RPC process is launched with (`--provider`), when the session selects no model. */
19
19
  readonly provider: string | undefined;
20
- /** Model pattern the `pi` RPC process is launched with (`--model`). */
20
+ /** Fallback model pattern for the `pi` RPC process (`--model`), used when the session selects none. */
21
21
  readonly model: string | undefined;
22
22
  /** Thinking/reasoning level for the model (`--model <id>:<level>` or set at runtime). */
23
23
  readonly thinkingLevel: string | undefined;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The plugin's own Remote: "which engine does this session run?" and "move this
3
+ * session to another engine", both answered from the plugin's own per-session
4
+ * record, the session's live agent, and the durable log through one shared read.
5
+ *
6
+ * The browser half cannot answer the first question honestly from what the
7
+ * harness hands a page. The client Session list carries `projectionValues`
8
+ * computed by `@deepseek-ai/dsh-api-session-controller`'s `projectionsFor()`,
9
+ * which returns a `SessionProjectionHints` snapshot — "every currently cached
10
+ * wire value", and partial by its own documentation: a cell the page's cache has
11
+ * not folded yet is simply absent. The engine a session ACTUALLY runs is not a
12
+ * cache fact at all, so the plugin becomes its own authority:
13
+ *
14
+ * - node half (here): a Typert Remote service, discovered by the host Gateway
15
+ * through its visible `typertRemote` binding and its `@Remote` method marks
16
+ * (`bindTypertRemote` / `@Remote` in `@deepseek-ai/dsh-typert-protocol`, the
17
+ * same mechanism `@deepseek-ai/dsh-agent-presets` uses for its own roster).
18
+ * Both endpoints go through {@link engineReportOfSession} /
19
+ * {@link RouterLoop.reportEngine} / {@link RouterLoop.selectEngine} — the very
20
+ * reads the router routes on — so what a session is driven by and what a
21
+ * session is shown as running cannot disagree, because there is one answer.
22
+ * The report carries the ONE case in which a session has two facts (a live
23
+ * agent and a record that did not displace it) as two fields rather than
24
+ * blending them; see {@link SessionEngineReport}.
25
+ * - browser half (`src/client/session-engine.ts`): mounts these endpoints as
26
+ * `remote.loopEngine.engine({ sessionId })` and
27
+ * `remote.loopEngine.select({ sessionId, engine })`, and renders only what
28
+ * they answer.
29
+ *
30
+ * Both methods take a PLAIN object parameter rather than an identity the Gateway
31
+ * resolves: typert resolves a parameter named `agent`/`session` into a LIVE
32
+ * object through its lookup providers, and these endpoints must answer for any
33
+ * PERSISTED session — including one no process has loaded.
34
+ *
35
+ * Wire contract (no codegen is involved; the Gateway derives the signature from
36
+ * the live method):
37
+ *
38
+ * - endpoint `loopEngine/engine`;
39
+ * - endpoint `loopEngine/select`;
40
+ * - one `src-json` argument per endpoint whose wire name is the method's own
41
+ * parameter name (`request`) — REQUIRED for the browser half to be
42
+ * understood, so the parameter name is part of the contract and must not be
43
+ * renamed or destructured (see the Gateway's `methodParameterNames`);
44
+ * - `engine` results in the plain-JSON {@link SessionEngineReport} — the engine
45
+ * the session ACTUALLY runs, plus (only while a live agent the record did not
46
+ * displace keeps running) the one the record names; `select` results in the
47
+ * plain-JSON {@link LoopEngineSelectResult} — a refusal is a VALUE, so a
48
+ * predictable "no" travels as data the surface can render (a
49
+ * {@link LoopEngineRefusalCode} plus the host's own sentence as detail, which
50
+ * is what lets the browser half show localized copy instead of a raw English
51
+ * one-liner), while only a malformed request is a `RemoteError`, and an
52
+ * outcome carrying `reload` tells the browser half to reload the page, because
53
+ * the switch was made to land by releasing the session's agent.
54
+ *
55
+ * @module dsh-loop-engine/engine-remote
56
+ */
57
+ import type { Context } from '@deepseek-ai/cordis';
58
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
59
+ import { SessionId } from '@deepseek-ai/dsh-session';
60
+ import { type LoopEngineId, type SessionEngine, type SessionEngineReport, type LoopEngineSelectResult } from './agent-preset-ids.ts';
61
+ /** Cordis service key AND wire namespace of the plugin's own Remote. */
62
+ export declare const LOOP_ENGINE_REMOTE_KEY = "loopEngine";
63
+ /** Endpoint method reporting one session's engine. */
64
+ export declare const LOOP_ENGINE_REMOTE_METHOD = "engine";
65
+ /** Endpoint method moving one session to another engine. */
66
+ export declare const LOOP_ENGINE_REMOTE_SELECT_METHOD = "select";
67
+ /** The wire request the reporting endpoint accepts. */
68
+ export interface LoopEngineRequest {
69
+ /** The session to report on. A plain string, never a resolved live object. */
70
+ readonly sessionId: string;
71
+ }
72
+ /**
73
+ * The wire request the switching endpoint accepts. The engine is typed as a
74
+ * string because that is what the wire carries; the endpoint validates it
75
+ * against {@link LOOP_ENGINE_IDS} before anything acts on it.
76
+ */
77
+ export interface LoopEngineSelectRequest {
78
+ /** The session to move. A plain string, never a resolved live object. */
79
+ readonly sessionId: string;
80
+ /** The engine that session should run. */
81
+ readonly engine: string;
82
+ }
83
+ /**
84
+ * The engine one session runs, read from the plugin's own record and the durable
85
+ * log (`engineOfSession`).
86
+ *
87
+ * The FALLBACK answer, not the whole one: it knows nothing about a live agent, so
88
+ * the endpoint uses it only while no router is mounted
89
+ * ({@link RouterSurfaceHolder}) — the mount window, in which no session is driven
90
+ * by this plugin anyway.
91
+ */
92
+ export type SessionEngineResolver = (sessionId: SessionId) => Promise<SessionEngine>;
93
+ /** Move one session to another engine, refusing with a reason rather than throwing. */
94
+ export type SessionEngineSelector = (sessionId: SessionId, engine: LoopEngineId) => Promise<LoopEngineSelectResult>;
95
+ /**
96
+ * The mounted loop router, as these two endpoints need it: the reporting read
97
+ * and the move, both of which only a live router can make.
98
+ *
99
+ * Structural rather than the class itself, so this module takes no build-time
100
+ * dependency on `./router-loop.ts` (which drags in the harness loop and every
101
+ * engine driver) and a test can stand in for it.
102
+ */
103
+ export interface RouterSurface {
104
+ /** Report what one session actually runs, plus the recorded engine when the two differ. */
105
+ reportEngine(sessionId: SessionId): Promise<SessionEngineReport>;
106
+ /** Move one session to another engine, or answer why it was not moved. */
107
+ selectEngine(sessionId: SessionId, engine: LoopEngineId): Promise<LoopEngineSelectResult>;
108
+ }
109
+ /**
110
+ * Where the two endpoints find the mounted router.
111
+ *
112
+ * A holder rather than a fixed reference because the router is mounted
113
+ * asynchronously — and not at all until the harness's live patch reload drops the
114
+ * base bundle's `agent-loop` row (`src/index.ts` `mountRouter`). Until then both
115
+ * endpoints answer from what a router-less process still knows: the record and
116
+ * the log. A session that is not driven by this plugin has no live agent to
117
+ * report anyway, so the answers agree.
118
+ */
119
+ export interface RouterSurfaceHolder {
120
+ /** The mounted router, or undefined while there is no router. */
121
+ current: RouterSurface | undefined;
122
+ }
123
+ /**
124
+ * `remote.loopEngine`: report one session's engine, and move it to another.
125
+ *
126
+ * Registered as a Cordis service under {@link LOOP_ENGINE_REMOTE_KEY} on the
127
+ * plugin's own fiber, so the binding and the method marks disappear with the
128
+ * plugin: a profile that mounts no Gateway never discovers it, and unmounting
129
+ * the plugin withdraws both.
130
+ */
131
+ export declare class LoopEngineRemote extends TypertRemoteService {
132
+ private readonly resolve;
133
+ private readonly router;
134
+ private readonly warn;
135
+ /**
136
+ * @param ctx - the context the service (and its lifetime) belongs to.
137
+ * @param resolve - the router-less read; normally
138
+ * `engineOfSession(ctx, sessionId, records)`.
139
+ * @param router - where both endpoints find the mounted loop router.
140
+ * @param warn - diagnostic sink for a session that could not be read.
141
+ */
142
+ constructor(ctx: Context, resolve: SessionEngineResolver, router: RouterSurfaceHolder, warn: (message: string) => void);
143
+ /**
144
+ * Report what one session runs — and, when its own record names a different
145
+ * engine than the live agent, that fact separately.
146
+ *
147
+ * The router answers when it is mounted, because the live agent outranks every
148
+ * record: only the router knows which engine built the agent driving this
149
+ * session now, and only the router can tell a record whose release did not take
150
+ * from one that already landed. Without a router (the mount-retry window) no
151
+ * session is driven by this plugin, so the record — i.e.
152
+ * {@link SessionEngineResolver} — is the whole answer.
153
+ *
154
+ * A session that cannot be read answers `unset` rather than throwing: a
155
+ * surface that asked about a session the durable log does not know must render
156
+ * "engine not recorded", not break the page it is rendered in. Failures are
157
+ * reported once on the host's log instead.
158
+ *
159
+ * A malformed REQUEST is a caller fault, not an unreadable session, and is
160
+ * refused the way every typert endpoint refuses one.
161
+ * @param request - the wire request carrying the session id.
162
+ * @returns the engine driving that session, plus the recorded engine it is not
163
+ * running when the two differ.
164
+ * @throws {RemoteError} with `gateway/bad-request` when no session id was sent.
165
+ */
166
+ engine(request: LoopEngineRequest): Promise<SessionEngineReport>;
167
+ /**
168
+ * Move one session to another engine.
169
+ *
170
+ * The move owns no policy of its own: the router decides whether this session
171
+ * can be moved right now and answers with a reason when it cannot, because
172
+ * only it knows the session's live agent, its turn state, and the teardown it
173
+ * would have to perform. This endpoint's job is the boundary — reject a
174
+ * malformed request the way every typert endpoint does, and hand a well-formed
175
+ * one to the selector.
176
+ * @param request - the wire request carrying the session id and target engine.
177
+ * @returns the engine now recorded for the session — with `reload: true` when
178
+ * its agent had to be released for the change to land, which the browser half
179
+ * acts on by reloading the page — or, as `ok: false`, the
180
+ * {@link LoopEngineRefusalCode} of the branch that refused it together with the
181
+ * host's own sentence about this session (`router-unmounted` is this
182
+ * endpoint's own, `session-closed` / `turn-running` / `subagent-session` /
183
+ * `not-driven` / `record-failed` / `rebuild-failed` come from the router's
184
+ * checks). The browser half localizes from the code and keeps the sentence as
185
+ * detail, so the code is part of this endpoint's contract even though the
186
+ * shape allows it to be absent.
187
+ * @throws {RemoteError} with `gateway/bad-request` when the session id is not
188
+ * a non-empty string or the engine is not an installed engine id.
189
+ */
190
+ select(request: LoopEngineSelectRequest): Promise<LoopEngineSelectResult>;
191
+ }
192
+ //# sourceMappingURL=engine-remote.d.ts.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Per-session command and skill surface of the hosted engines.
3
+ *
4
+ * A hosted engine owns its session's slash-command menu and skill catalog: the
5
+ * engine expands `/name` itself and reads its own instruction files, so the
6
+ * plugin bridges both into the session the engine serves and nothing more.
7
+ *
8
+ * The registrations are made through the AGENT's own context, which is what
9
+ * makes them session-scoped: `commands.register` and
10
+ * `skills.registerProvider` file a definition into the layer of the calling
11
+ * context's scope (`packages/core/scope/src/store.ts` `effect()` keys the layer
12
+ * by `scopeOf(ctx)`), and an agent's scope key is the agent itself
13
+ * (`createScope(loopCtx, this)` in the default loop; the hosted drivers do the
14
+ * same). Two sessions running different engines therefore never see each
15
+ * other's menus, and the whole surface disappears with the agent's scope — no
16
+ * plugin-side bookkeeping, no leak across an engine switch.
17
+ *
18
+ * @module dsh-loop-engine/engine-surface
19
+ */
20
+ import type { Agent } from '@deepseek-ai/dsh-agent';
21
+ import type { HostedEngineId } from './settings.ts';
22
+ /**
23
+ * Bridge one hosted engine's commands and skills into the session it serves.
24
+ *
25
+ * Best-effort like the rest of the plugin's host-service use: a profile without
26
+ * the commands or skills registry simply has no surface to extend. A name that
27
+ * collides with an already-registered command in the same layer is skipped with
28
+ * a warning rather than failing the agent: the engine expands the raw `/name`
29
+ * line itself, so a shyer menu beats an agent that refuses to start.
30
+ *
31
+ * @param agent - the freshly built agent whose scope owns the registrations.
32
+ * @param engine - the hosted engine driving that agent.
33
+ * @param warn - sink for the skip diagnostics.
34
+ */
35
+ export declare function registerEngineSurface(agent: Agent, engine: HostedEngineId, warn: (message: string) => void): void;
36
+ //# sourceMappingURL=engine-surface.d.ts.map