dsh-loop-engine 0.1.5-rc1 → 0.1.5-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 (35) hide show
  1. package/README.md +88 -5
  2. package/README.zh.md +74 -0
  3. package/lib/client.js +158 -0
  4. package/lib/index.js +1020 -1486
  5. package/lib/invariant.js +6 -3
  6. package/lib/types/client/turn-status.d.ts +43 -0
  7. package/lib/types/driver-core/agents-md-skill-provider.d.ts +72 -0
  8. package/lib/types/driver-core/hosted-loop-factory.d.ts +119 -0
  9. package/lib/types/driver-core/ownership.d.ts +6 -6
  10. package/lib/types/driver-core/permission-knobs.d.ts +1 -1
  11. package/lib/types/driver-core/prompt.d.ts +1 -1
  12. package/lib/types/driver-core/skill-inject.d.ts +2 -2
  13. package/lib/types/engine-claude/agent.d.ts +22 -0
  14. package/lib/types/engine-claude/loop.d.ts +7 -56
  15. package/lib/types/engine-claude/mapping.d.ts +28 -3
  16. package/lib/types/engine-codex/agent.d.ts +46 -3
  17. package/lib/types/engine-codex/appserver/client.d.ts +16 -0
  18. package/lib/types/engine-codex/loop.d.ts +7 -56
  19. package/lib/types/engine-codex/permission.d.ts +100 -6
  20. package/lib/types/engine-codex/skills.d.ts +7 -8
  21. package/lib/types/engine-kimi/acp/client.d.ts +33 -3
  22. package/lib/types/engine-kimi/acp/mapping.d.ts +36 -7
  23. package/lib/types/engine-kimi/acp/types.d.ts +41 -2
  24. package/lib/types/engine-kimi/agent.d.ts +65 -8
  25. package/lib/types/engine-kimi/loop.d.ts +7 -56
  26. package/lib/types/engine-kimi/skills.d.ts +9 -14
  27. package/lib/types/engine-pi/agent.d.ts +23 -4
  28. package/lib/types/engine-pi/loop.d.ts +7 -56
  29. package/lib/types/engine-pi/permission.d.ts +16 -12
  30. package/lib/types/engine-pi/skills.d.ts +6 -14
  31. package/lib/types/patch-manager.d.ts +7 -0
  32. package/lib/types/provider-route.d.ts +16 -7
  33. package/lib/types/settings.d.ts +4 -1
  34. package/lib/types/skills.d.ts +6 -14
  35. package/package.json +1 -1
@@ -42,14 +42,33 @@ export declare class PiAgent implements Agent {
42
42
  private requestHeaderLogged;
43
43
  /** Agent-lifecycle-local counter naming each streamed attempt. */
44
44
  private streamAttempts;
45
- /** Lazily created RPC client, reused across steps and released on scope teardown. */
45
+ /** This step's RPC child; released by the step teardown and the scope teardown. */
46
46
  private rpc;
47
- /** The spawn spec the cached client was built from; a change forces a respawn. */
48
- private lastSpec;
47
+ /**
48
+ * Tool results logged into the currently open step. A result means the
49
+ * segment that requested the call is finished, so the next assistant content
50
+ * opens the next step (see {@link beginSegment}).
51
+ */
52
+ private stepSettledTools;
53
+ /**
54
+ * Rotate to the next step when the segment that ran a tool has finished, so
55
+ * each assistant segment lands in its own step.
56
+ *
57
+ * Called as new assistant content begins. A step holding a settled tool
58
+ * result means the previous segment is complete, and the content about to be
59
+ * written belongs to the next one. Rotating at a message boundary keeps calls
60
+ * announced together — one model turn — in one step.
61
+ * @param phase - the running phase carrying the open step's position.
62
+ */
63
+ private beginSegment;
49
64
  constructor(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session, config: ResolvedConfig, spawn: PiSpawnCapability, bin: string, catalog: {
50
65
  readonly entries: readonly PiModelEntry[];
51
66
  });
52
- /** Return the cached RPC client, respawning when the spec or process changed. */
67
+ /**
68
+ * Open this step's RPC child. The Pi RPC process is single-session, so a step
69
+ * never reuses one: the step teardown disposes it and the next step respawns
70
+ * a fresh child.
71
+ */
53
72
  private rpcClient;
54
73
  get status(): AgentStatus;
55
74
  /** Commit a phase and publish its externally visible status transition. */
@@ -10,13 +10,15 @@
10
10
  *
11
11
  * @module dsh-loop-engine/engine-pi
12
12
  */
13
- import { Service } from '@deepseek-ai/cordis';
14
13
  import type { Context } from '@deepseek-ai/cordis';
15
14
  import z from '@deepseek-ai/schemastery';
16
- import type { AgentFactory, AgentHandle, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent';
15
+ import type { AgentOptions } from '@deepseek-ai/dsh-agent';
16
+ import type { Session, SessionId } from '@deepseek-ai/dsh-session';
17
+ import { PiAgent } from './agent.ts';
17
18
  import type { PiModelEntry } from './probe.ts';
18
19
  import type { PiProcess, PiSpawnSpec } from './rpc/client.ts';
19
20
  import type { PiSandboxMode, ResolvedConfig } from './types.ts';
21
+ import { HostedLoopFactory } from '../driver-core/hosted-loop-factory.ts';
20
22
  /** Pi CLI sandbox modes a deployment may pin. */
21
23
  export declare const PI_SANDBOX_MODES: readonly PiSandboxMode[];
22
24
  /** Grace in milliseconds for Pi process-tree termination. */
@@ -58,14 +60,9 @@ declare module '@deepseek-ai/cordis' {
58
60
  * prepare, run setup, then publish through both registries, announce, and emit
59
61
  * `agent/session-start`.
60
62
  */
61
- export declare class PiLoop extends Service implements AgentFactory {
63
+ export declare class PiLoop extends HostedLoopFactory<ResolvedConfig, PiAgent> {
62
64
  /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
63
65
  static inject: string[];
64
- /** Validated configuration owned by the loop plugin. */
65
- readonly config: ResolvedConfig;
66
- private readonly ownership;
67
- /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
68
- private readonly runtime;
69
66
  /** Process-tree spawn capability handed to every agent, sandboxed by the subprocess seam. */
70
67
  readonly spawn: (spec: PiSpawnSpec) => PiProcess;
71
68
  /** Resolved Pi CLI entrypoint; `argv[0]` of every Pi RPC child. */
@@ -73,53 +70,7 @@ export declare class PiLoop extends Service implements AgentFactory {
73
70
  /** Discovered Pi model catalog, forwarded to each agent so it can validate the session-selected model against what pi can actually serve. */
74
71
  private readonly catalog;
75
72
  constructor(ctx: Context, config: Config);
76
- /**
77
- * Construct the driver, scope, and one memoized reverse teardown for a new
78
- * agent. The teardown is registered with the factory and the owner fiber
79
- * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
80
- * fuses caller cancellation with lifecycle teardown for setup awaits.
81
- */
82
- private prepare;
83
- /** Prepare one Agent around an acquired Session, run setup, and publish it. */
84
- private setupAndPublish;
85
- /**
86
- * Create an agent and session under one caller-supplied identity, owned by
87
- * the accessing fiber. When a persistence backend is mounted, the session's
88
- * durable identity is stored before publication.
89
- * @param ownerCtx - caller context that structurally owns the lifecycle.
90
- * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
91
- * @returns the published handle.
92
- */
93
- createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;
94
- /**
95
- * Take a fresh session's write ownership when persistence is mounted.
96
- * Nothing is appended here: the constructor seed (which never re-emits
97
- * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
98
- * publication commit point, so a failed or cancelled setup closes an
99
- * unmaterialized handle and leaves no stored residue — the same id can be
100
- * created again.
101
- * @param session - the unpublished session to store.
102
- * @param signal - optional cancellation forwarded to the backend create.
103
- * @returns the owned handle and stored cursor, or `undefined` without a backend.
104
- */
105
- private createStoredSession;
106
- /**
107
- * Durably store the session events appended since the last stored cursor.
108
- * Pre-publication appends (constructor seed markers, setup-window events)
109
- * never re-emit through `session/event`, so publication must flush them
110
- * through the handle before live events start routing into it.
111
- * @param stored - the session's owned handle and stored cursor, if any.
112
- * @param session - the unpublished session whose suffix is stored.
113
- */
114
- private appendUnstoredSuffix;
115
- /**
116
- * Resume an owned agent from the configured persistence service.
117
- * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
118
- * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
119
- * @returns the published handle.
120
- */
121
- resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;
122
- /** Resume through an explicit persistence service. */
123
- private resumeWith;
73
+ /** Construct the Pi RPC driver for one prepared session. */
74
+ protected buildAgent(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session): PiAgent;
124
75
  }
125
76
  //# sourceMappingURL=loop.d.ts.map
@@ -2,13 +2,12 @@
2
2
  * Mapping from the dsh session's durable permission knobs to one Pi RPC
3
3
  * process's runtime stance. Pi carries no native permission system — "runs
4
4
  * with the permissions of the user" — so the driver cannot ask it to sandbox or
5
- * approve. The only available boundary is the process environment: the driver
6
- * either wraps the whole `pi --mode rpc` child in the dsh subprocess sandbox
7
- * and prunes its `--tools`, or (full access) lets it run under the dsh user.
8
- * The fold mirrors the codex bridge, mapping the session's `sandbox/mode` and
9
- * `approval/policy` events directly:
10
- * - full access → `danger-full-access`, no tool pruning (native tools);
11
- * - `workspace-write` → sandbox wrap with a write-capable tool set;
5
+ * approve. What is left is the `--tools` allowlist: the child runs under the
6
+ * dsh user either way (the subprocess seam carries no confinement of its own),
7
+ * so withholding a tool IS the stance. The fold mirrors the codex bridge,
8
+ * mapping the session's `sandbox/mode` and `approval/policy` events directly:
9
+ * - full access → `danger-full-access`, no pruning (Pi's native tools);
10
+ * - `workspace-write` → a write-capable set, still without a shell;
12
11
  * - an `ask` policy → degraded to a read-only denial (Pi has no request
13
12
  * callback, so interactive approval can only become a rejection);
14
13
  * - anything else fails closed → `read-only`.
@@ -19,17 +18,22 @@ import type { PermissionEvent } from '../driver-core/permission-knobs.ts';
19
18
  import type { PiSandboxMode } from './types.ts';
20
19
  /** The runtime stance one Pi RPC process should run under. */
21
20
  export interface PiPermission {
22
- /** Sandbox mode driving whether the child is wrapped in the dsh sandbox. */
21
+ /** The resolved sandbox stance; selects the tool set, not a process sandbox. */
23
22
  readonly sandboxMode: PiSandboxMode;
24
- /** The `--tools` allowlist; empty means "use Pi's native tools" (no pruning). */
23
+ /** The `--tools` allowlist — the stance's only enforcement; empty means no pruning. */
25
24
  readonly tools: readonly string[];
26
25
  }
27
- /** Conservative unattended default: read-only sandbox, no write/exec tools. */
26
+ /**
27
+ * Conservative unattended default: read-only sandbox, no write/exec tools. The
28
+ * allowlist names pi's built-ins (`read`, `bash`, `edit`, `write`) and nothing
29
+ * else — a name pi does not know matches no tool at all, silently, so a list
30
+ * carrying invented entries would enforce less than it claims to.
31
+ */
28
32
  export declare const DEFAULT_PI_PERMISSION: PiPermission;
29
33
  /**
30
34
  * Derive the `--tools` allowlist for a given sandbox stance. Full access prunes
31
- * nothing; `workspace-write` allows a write-capable set; `read-only` allows read
32
- * and search only.
35
+ * nothing; `workspace-write` allows a write-capable set; `read-only` allows
36
+ * reading and nothing else. Only pi's own built-in tool names appear here.
33
37
  * @param mode - the resolved sandbox stance.
34
38
  * @returns the tool set to pass as `--tools`.
35
39
  */
@@ -19,9 +19,13 @@
19
19
  * at composition time — the filesystem subset above is authoritative for the
20
20
  * web menu.
21
21
  *
22
+ * The discovery algorithm itself lives in {@link AgentsMdSkillProvider}; this
23
+ * module supplies only Pi's locations and ranks.
24
+ *
22
25
  * @module dsh-loop-engine/engine-pi/skills
23
26
  */
24
- import type { SkillCandidate, SkillDefinition, SkillLookupOptions, SkillProvider, SkillProviderControl } from '../skills.ts';
27
+ import { AgentsMdSkillProvider } from '../driver-core/agents-md-skill-provider.ts';
28
+ import type { SkillProviderControl } from '../skills.ts';
25
29
  /**
26
30
  * Resolve the pi config directory, honoring the `PI_CODING_AGENT_DIR`
27
31
  * environment override and falling back to `~/.pi/agent`.
@@ -36,20 +40,8 @@ export declare function piAgentDir(): string;
36
40
  * - project `.pi/skills/` and user `~/.pi/agent/skills/` — each `SKILL.md`
37
41
  * entry surfaced under its own name.
38
42
  */
39
- export declare class PiSkillProvider implements SkillProvider {
40
- private readonly control;
41
- readonly name = "pi";
43
+ export declare class PiSkillProvider extends AgentsMdSkillProvider {
42
44
  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
45
  }
54
46
  export default PiSkillProvider;
55
47
  //# sourceMappingURL=skills.d.ts.map
@@ -42,6 +42,13 @@ export declare const MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --
42
42
  export declare function renderManagedBlock(engine: LoopEngineId): string;
43
43
  /** Whether a patch-file text contains the managed block span. */
44
44
  export declare function hasManagedBlock(text: string): boolean;
45
+ /**
46
+ * The engine id the managed block's begin marker names, when this build knows
47
+ * it. `undefined` covers both "no managed block" and "the block names an engine
48
+ * this build does not recognize" — the second being the state where the block
49
+ * still disables the base `agent-loop` row while no factory can take the slot.
50
+ */
51
+ export declare function managedBlockEngineOf(text: string): LoopEngineId | undefined;
45
52
  /** Derive the current engine from a patch-file text by the managed block's begin marker. */
46
53
  export declare function currentEngineOf(text: string): LoopEngineId;
47
54
  /**
@@ -7,8 +7,9 @@
7
7
  * host derives a session's model selection from that header and refuses a turn
8
8
  * whose provider no registered adapter serves, so without a placeholder route
9
9
  * the SECOND prompt of every hosted session fails with `model-unavailable`.
10
- * The placeholder serves the label while advertising no models; catalog groups
11
- * that advertise nothing are dropped, so the model picker is unchanged.
10
+ * The placeholder serves the label and advertises no models unless a deployment
11
+ * injects a catalog (only `pi` does, from its model probe); catalog groups that
12
+ * advertise nothing are dropped, so the picker is otherwise unchanged.
12
13
  *
13
14
  * @module dsh-loop-engine/provider-route
14
15
  */
@@ -29,10 +30,11 @@ export interface HostedEngineRouteAdapterOptions {
29
30
  }
30
31
  /**
31
32
  * Placeholder adapter serving one hosted engine's provider route label. It
32
- * inherits the empty catalog and default metadata (the engine's model is not a
33
- * harness-selectable endpoint), and {@link stream} fails loud: a call reaching
34
- * it means a real model query was routed to an engine that owns its model
35
- * natively — a wiring bug, not a request to serve.
33
+ * advertises the injected catalog when a deployment provides one and stays
34
+ * empty otherwise (the engine's model is not itself a harness-selectable
35
+ * endpoint), and {@link stream} fails loud: a call reaching it means a real
36
+ * model query was routed to an engine that owns its model natively — a wiring
37
+ * bug, not a request to serve.
36
38
  */
37
39
  export declare class HostedEngineRouteAdapter extends LlmAdapter {
38
40
  private readonly label;
@@ -42,7 +44,14 @@ export declare class HostedEngineRouteAdapter extends LlmAdapter {
42
44
  * @param options - optional catalog source; omit for an empty catalog.
43
45
  */
44
46
  constructor(label: string, options?: HostedEngineRouteAdapterOptions);
45
- /** Advertise the injected Pi models (if any) under this route's provider label. */
47
+ /**
48
+ * Advertise the injected Pi models (if any) under this route's provider label.
49
+ *
50
+ * The model is what the picker shows at the top level, so `name` is the bare
51
+ * model; the `provider/model` composite stays the submitted `id`, which is
52
+ * both what the engine receives as `--model` and how the driver validates a
53
+ * session-selected model against its catalog.
54
+ */
46
55
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
47
56
  stream(_options: GenerateOptions): AsyncIterable<StreamChunk>;
48
57
  }
@@ -24,7 +24,10 @@ export interface LoopEngineSettings {
24
24
  /** Whether the composer's loop engine picker is shown on the chat page. */
25
25
  showInComposer: boolean;
26
26
  }
27
- /** Schema of the loop engine settings section. */
27
+ /**
28
+ * Schema of the loop engine settings section. The engine union derives from
29
+ * {@link LOOP_ENGINE_IDS}, so adding an engine there also admits it here.
30
+ */
28
31
  export declare const LOOP_ENGINE_SETTINGS_SCHEMA: z<LoopEngineSettings>;
29
32
  /** The shared literal branded as a settings namespace on the node side. */
30
33
  export declare function loopEngineSettingsNamespace(): SettingsNamespace;
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @module dsh-loop-engine/skills
8
8
  */
9
+ import type { SkillDefinition } from './driver-core/skill-inject.ts';
9
10
  export interface SkillInvocationPolicy {
10
11
  readonly modelInvocable: boolean;
11
12
  readonly userInvocable: boolean;
@@ -26,20 +27,11 @@ export interface SkillCandidate {
26
27
  readonly path: string;
27
28
  };
28
29
  }
29
- export interface SkillDefinition {
30
- readonly name: string;
31
- readonly description: string;
32
- readonly whenToUse?: string;
33
- readonly invocation: SkillInvocationPolicy;
34
- readonly source: SkillSource;
35
- readonly provider: string;
36
- readonly content: string;
37
- readonly path?: string;
38
- readonly resourceBase?: {
39
- readonly kind: string;
40
- readonly path: string;
41
- };
42
- }
30
+ /**
31
+ * The providers' loaded-skill shape is the driver-core one, so the skill
32
+ * providers and the engine agents that consume them cannot drift apart.
33
+ */
34
+ export type { SkillDefinition } from './driver-core/skill-inject.ts';
43
35
  export interface SkillLookupOptions {
44
36
  readonly cwd?: string;
45
37
  readonly signal?: AbortSignal;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-loop-engine",
3
3
  "description": "Web-switchable agent loop engine selection for the DeepSeek Harness - out-of-tree plugin (Claude Code / Codex / Pi / Kimi Code drivers) maintained by @kuun993, zero main-repo changes",
4
- "version": "0.1.5-rc1",
4
+ "version": "0.1.5-rc2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },