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
@@ -9,11 +9,13 @@
9
9
  *
10
10
  * @module dsh-loop-engine/engine-codex
11
11
  */
12
- import { Service } from '@deepseek-ai/cordis';
13
12
  import type { Context } from '@deepseek-ai/cordis';
14
13
  import z from '@deepseek-ai/schemastery';
15
- import type { AgentFactory, AgentHandle, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent';
14
+ import type { AgentOptions } from '@deepseek-ai/dsh-agent';
15
+ import type { Session, SessionId } from '@deepseek-ai/dsh-session';
16
+ import { CodexAgent } from './agent.ts';
16
17
  import type { CodexApprovalPolicy, CodexSandboxMode, ResolvedConfig } from './types.ts';
18
+ import { HostedLoopFactory } from '../driver-core/hosted-loop-factory.ts';
17
19
  /** Codex CLI sandbox modes a deployment may pin. */
18
20
  export declare const CODEX_SANDBOX_MODES: readonly CodexSandboxMode[];
19
21
  /** Codex CLI approval policies a deployment may pin. */
@@ -53,62 +55,11 @@ declare module '@deepseek-ai/cordis' {
53
55
  * transaction: prepare, run setup, then publish through both registries,
54
56
  * announce, and emit `agent/session-start`.
55
57
  */
56
- export declare class CodexLoop extends Service implements AgentFactory {
58
+ export declare class CodexLoop extends HostedLoopFactory<ResolvedConfig, CodexAgent> {
57
59
  /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
58
60
  static inject: string[];
59
- /** Validated configuration owned by the loop plugin. */
60
- readonly config: ResolvedConfig;
61
- private readonly ownership;
62
- /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
63
- private readonly runtime;
64
61
  constructor(ctx: Context, config: Config);
65
- /**
66
- * Construct the driver, scope, and one memoized reverse teardown for a new
67
- * agent. The teardown is registered with the factory and the owner fiber
68
- * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
69
- * fuses caller cancellation with lifecycle teardown for setup awaits.
70
- */
71
- private prepare;
72
- /** Prepare one Agent around an acquired Session, run setup, and publish it. */
73
- private setupAndPublish;
74
- /**
75
- * Create an agent and session under one caller-supplied identity, owned by
76
- * the accessing fiber. When a persistence backend is mounted, the session's
77
- * durable identity is stored before publication.
78
- * @param ownerCtx - caller context that structurally owns the lifecycle.
79
- * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
80
- * @returns the published handle.
81
- */
82
- createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;
83
- /**
84
- * Take a fresh session's write ownership when persistence is mounted.
85
- * Nothing is appended here: the constructor seed (which never re-emits
86
- * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
87
- * publication commit point, so a failed or cancelled setup closes an
88
- * unmaterialized handle and leaves no stored residue — the same id can be
89
- * created again.
90
- * @param session - the unpublished session to store.
91
- * @param signal - optional cancellation forwarded to the backend create.
92
- * @returns the owned handle and stored cursor, or `undefined` without a backend.
93
- */
94
- private createStoredSession;
95
- /**
96
- * Durably store the session events appended since the last stored cursor.
97
- * Pre-publication appends (constructor seed markers, setup-window events)
98
- * never re-emit through `session/event`, so publication must flush them
99
- * through the handle before live events start routing into it.
100
- * @param stored - the session's owned handle and stored cursor, if any.
101
- * @param session - the unpublished session whose suffix is stored.
102
- */
103
- private appendUnstoredSuffix;
104
- /**
105
- * Resume an owned agent from the configured persistence service.
106
- * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
107
- * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
108
- * @returns the published handle.
109
- */
110
- resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;
111
- /** Resume through an explicit persistence service. */
112
- private resumeWith;
62
+ /** Construct the Codex driver for one prepared session. */
63
+ protected buildAgent(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session): CodexAgent;
113
64
  }
114
65
  //# sourceMappingURL=loop.d.ts.map
@@ -1,14 +1,23 @@
1
1
  /**
2
2
  * Mapping from the dsh session's durable permission knobs to one Codex query's
3
- * declarative permission stance. Codex has no interactive approval callback:
4
- * permissions are the `sandboxMode` + `approvalPolicy` pair chosen when the
5
- * thread starts, so the fold maps the session's `sandbox/mode` and
6
- * `approval/policy` events directly, mirroring the web surface's presets:
3
+ * declarative permission stance, plus the replies to every server-initiated
4
+ * interaction Codex can raise. Codex surfaces interactive approval as
5
+ * server-initiated JSON-RPC requests answered through the dsh approval seam
6
+ * (see `approvalReason` / `resolveApprovalRequest` below), while the thread
7
+ * still starts with the `sandboxMode` + `approvalPolicy` pair chosen at
8
+ * creation. The fold maps the session's `sandbox/mode` and `approval/policy`
9
+ * events directly, mirroring the web surface's presets:
7
10
  * - full access → `danger-full-access` + `never` (no native checks at all),
8
- * - an `ask` policy → `workspace-write` + `on-request` (the CLI's own
9
- * interactive prompt degrades to a denial in the unattended dsh runtime),
11
+ * - an `ask` policy → `workspace-write` + `on-request` (approvals are then
12
+ * routed to the dsh approval seam, failing closed when it is absent),
10
13
  * - anything else fails closed → `read-only` + `never`.
11
14
  *
15
+ * The other two interactions (`item/tool/requestUserInput` and
16
+ * `mcpServer/elicitation/request`) have no approval to grant: one asks the human
17
+ * a question, the other asks for MCP input. Both are mapped here so the driver
18
+ * answers them in-vocabulary instead of failing them with a protocol error that
19
+ * the model reads as a broken tool.
20
+ *
12
21
  * @module dsh-loop-engine/engine-codex/permission
13
22
  */
14
23
  import type { PermissionEvent } from '../driver-core/permission-knobs.ts';
@@ -20,6 +29,69 @@ export interface CodexPermission {
20
29
  }
21
30
  /** Conservative unattended default: read-only sandbox, never ask. */
22
31
  export declare const DEFAULT_CODEX_PERMISSION: CodexPermission;
32
+ /**
33
+ * One question to put to the human, in the shape the dsh user-questions seam
34
+ * accepts. Declared inline (a structural subset) to avoid a peer dep on
35
+ * `@deepseek-ai/dsh-user-questions`, matching the approval seam's treatment.
36
+ */
37
+ export interface UserQuestionItem {
38
+ /** Stable question id, echoed back by the answer and by our reply. */
39
+ readonly id: string;
40
+ /** The question to display. */
41
+ readonly question: string;
42
+ /** Optional short heading/group label. */
43
+ readonly header?: string;
44
+ /** Optional choices a UI renders as a menu. */
45
+ readonly options?: readonly {
46
+ readonly label: string;
47
+ readonly description?: string;
48
+ }[];
49
+ }
50
+ /** The human's answer, in the shape the dsh user-questions seam returns. */
51
+ export interface UserQuestionAnswer {
52
+ /** One entry per answered question; unanswered questions are simply absent. */
53
+ readonly answers: readonly {
54
+ readonly id: string;
55
+ readonly selected: readonly string[];
56
+ /** Optional free-text answer, carried alongside `selected`. */
57
+ readonly custom?: string;
58
+ }[];
59
+ }
60
+ /** The `item/tool/requestUserInput` result Codex expects. */
61
+ export interface CodexUserInputResponse {
62
+ /** Answer lists keyed by question id; an empty map means "no answers given". */
63
+ readonly answers: Record<string, {
64
+ readonly answers: string[];
65
+ }>;
66
+ }
67
+ /**
68
+ * Read the answerable questions of one `item/tool/requestUserInput` request. The
69
+ * wire is untrusted, so an entry with no usable id or wording is dropped: the
70
+ * reply is keyed by question id, and a synthesized key would answer a question
71
+ * Codex never asked.
72
+ * @param params - the request params.
73
+ * @returns the questions to put to the human, possibly none.
74
+ */
75
+ export declare function userInputQuestions(params: unknown): UserQuestionItem[];
76
+ /**
77
+ * Project one human answer onto Codex's response shape. A free-text answer rides
78
+ * along as a further entry of the question's answer list, and no answer at all
79
+ * (no seam, or the human dismissed it) becomes an empty map — the honest "asked,
80
+ * answered nothing", never an invented answer.
81
+ * @param answer - the seam's answer, or undefined when none was obtained.
82
+ * @returns the response payload.
83
+ */
84
+ export declare function userInputResponse(answer: UserQuestionAnswer | undefined): CodexUserInputResponse;
85
+ /**
86
+ * The reply to one `mcpServer/elicitation/request`. An unattended driver cannot
87
+ * render an elicitation form, so it declines — the same stance the Claude Code
88
+ * driver takes for the equivalent callback, and the one answer that never
89
+ * fabricates input the user did not give.
90
+ * @returns the response payload.
91
+ */
92
+ export declare function elicitationResponse(): {
93
+ action: 'decline';
94
+ };
23
95
  /**
24
96
  * Resolve the session's effective Codex permission stance. Full access wins
25
97
  * outright; otherwise an `ask` policy maps to the CLI's on-request approval
@@ -29,4 +101,26 @@ export declare const DEFAULT_CODEX_PERMISSION: CodexPermission;
29
101
  * @returns the stance one query should run under.
30
102
  */
31
103
  export declare function resolveSessionPermission(events: readonly PermissionEvent[]): CodexPermission;
104
+ /** The closed outcome of one dsh approval request (mirrors the dsh-user-approval seam). */
105
+ export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
106
+ /** Short tool identity for the dsh approval request carrying one native Codex approval. */
107
+ export declare function approvalToolName(method: string): string;
108
+ /** Human-readable reason for one native Codex approval request. */
109
+ export declare function approvalReason(method: string, params: unknown): string;
110
+ /** The native decision a command/file-change approval resolves to. */
111
+ export declare function approvalDecision(outcome: ApprovalOutcome): 'accept' | 'decline';
112
+ /** The native permissions-request response for one approval outcome. */
113
+ export declare function permissionsGrant(outcome: ApprovalOutcome, requested: unknown): {
114
+ permissions: unknown;
115
+ scope: 'turn';
116
+ };
117
+ /** Build the JSON-RPC reply payload for one inbound Codex approval request. */
118
+ export declare function resolveApprovalRequest(method: string, params: unknown, outcome: ApprovalOutcome): {
119
+ result: unknown;
120
+ } | {
121
+ error: {
122
+ code: number;
123
+ message: string;
124
+ };
125
+ };
32
126
  //# sourceMappingURL=permission.d.ts.map
@@ -9,21 +9,20 @@
9
9
  * contents, so the dsh skill-injection seam (`/name` gestures) can carry it
10
10
  * into the prompt.
11
11
  *
12
+ * The discovery algorithm itself lives in {@link AgentsMdSkillProvider}; this
13
+ * module supplies only Codex's locations and ranks — and, having no skills
14
+ * catalog, no `skills` entry.
15
+ *
12
16
  * @module dsh-loop-engine/engine-codex/skills
13
17
  */
14
- import type { SkillCandidate, SkillDefinition, SkillLookupOptions, SkillProvider, SkillProviderControl } from '../skills.ts';
18
+ import { AgentsMdSkillProvider } from '../driver-core/agents-md-skill-provider.ts';
19
+ import type { SkillProviderControl } from '../skills.ts';
15
20
  /**
16
21
  * Skill provider that discovers `AGENTS.md` from every directory between the
17
22
  * project cwd and the git root, plus the user home `~/.codex/AGENTS.md`.
18
23
  */
19
- export declare class CodexSkillProvider implements SkillProvider {
20
- private readonly control;
21
- readonly name = "codex";
24
+ export declare class CodexSkillProvider extends AgentsMdSkillProvider {
22
25
  constructor(control: SkillProviderControl);
23
- list(options: SkillLookupOptions): Promise<readonly SkillCandidate[]>;
24
- get(candidate: SkillCandidate, _options: SkillLookupOptions): Promise<SkillDefinition | undefined>;
25
- /** One merged `agents-md` candidate for a ranked file set. */
26
- private agentsCandidate;
27
26
  }
28
27
  export default CodexSkillProvider;
29
28
  //# sourceMappingURL=skills.d.ts.map
@@ -13,9 +13,35 @@
13
13
  * @module dsh-loop-engine/engine-kimi/acp/client
14
14
  */
15
15
  import type { KimiProcess, KimiSpawnCapability, KimiSpawnSpec } from '../process.ts';
16
- import { type AcpFrame, type AcpUpdate } from './types.ts';
16
+ import { type AcpFrame, type AcpPermissionOption, type AcpPermissionResponse, type AcpUpdate } from './types.ts';
17
17
  /** How the client answers one `session/request_permission`. */
18
18
  export type AcpPermissionHandler = (request: AcpFrame) => boolean | Promise<boolean>;
19
+ /**
20
+ * The answerable options of one `session/request_permission` frame. The wire is
21
+ * untrusted, so entries without a usable `optionId` are dropped rather than
22
+ * echoed back.
23
+ * @param frame - the reverse-RPC request frame.
24
+ * @returns the options the client may answer with.
25
+ */
26
+ export declare function permissionOptionsOf(frame: AcpFrame): readonly AcpPermissionOption[];
27
+ /**
28
+ * Encode one approval decision as the ACP `RequestPermissionResponse` the agent
29
+ * correlates against the options it advertised. The agent reads a terminal
30
+ * *option*, never a boolean: an outcome it cannot resolve (or a response that
31
+ * fails to parse at all) is reported to the model as a user rejection, so a
32
+ * decision that no single option expresses becomes `cancelled` — the honest
33
+ * "this client has no answer". Kimi re-uses this RPC for its question and
34
+ * plan-review bridges, which offer one `allow_once` option per choice; picking
35
+ * one of those would answer a question no human was asked, so any ambiguous
36
+ * set (zero or several `allow_once`/`reject_once` candidates) cancels instead.
37
+ * `allow_once` is preferred over `allow_always` because the decision is re-read
38
+ * from the session knobs on every request: letting the agent cache a session
39
+ * grant would outlive a mid-session switch back to `ask`.
40
+ * @param approved - whether the driver approves the request.
41
+ * @param options - the options the agent advertised for this request.
42
+ * @returns the result payload to send back.
43
+ */
44
+ export declare function permissionResponse(approved: boolean, options: readonly AcpPermissionOption[]): AcpPermissionResponse;
19
45
  /** Callback receiving every non-response event line. */
20
46
  export type AcpUpdateHandler = (update: AcpUpdate) => void;
21
47
  /**
@@ -61,8 +87,12 @@ export declare class AcpClient {
61
87
  prompt(sessionId: string, text: string): Promise<unknown>;
62
88
  /** Cancel the active turn in a session (fire-and-forget). */
63
89
  cancel(sessionId: string): void;
64
- /** Answer a pending `session/request_permission`. */
65
- respondPermission(id: number, approved: boolean): void;
90
+ /**
91
+ * Answer a pending `session/request_permission`.
92
+ * @param id - the reverse-RPC request id.
93
+ * @param response - the ACP outcome to answer with.
94
+ */
95
+ respondPermission(id: number, response: AcpPermissionResponse): void;
66
96
  /** Consume every buffered update as an async generator. */
67
97
  updates(): AsyncGenerator<AcpUpdate, void, void>;
68
98
  /** Seal the client and request child termination. */
@@ -3,11 +3,15 @@
3
3
  *
4
4
  * Kimi streams incremental assistant text (`agent_message_chunk`), incremental
5
5
  * thinking (`agent_thought_chunk`), a tool-call announcement (`tool_call`) and its
6
- * progress/result stream (`tool_call_update`). This module is pure: it classifies
7
- * an update, extracts chunk deltas, and projects the tool-call identity/result so
8
- * the agent can fold them into the durable log. Content blocks use the observed
9
- * kimi `{ type: 'content', content: { type: 'text', text } }` nesting; unknown
10
- * block types are ignored.
6
+ * progress/result updates (`tool_call_update`). Assistant text and thinking are
7
+ * Deltas; a tool update's content is a whole Snapshot that replaces the previous
8
+ * one. A tool call's identity arrives on the announcement but its input
9
+ * (`rawInput`) does not — only a later update carries it. This module is pure: it
10
+ * classifies an update, extracts chunk deltas, and projects the tool-call
11
+ * identity/input/content/result so the agent can fold them into the durable log.
12
+ * Content blocks use the observed kimi
13
+ * `{ type: 'content', content: { type: 'text', text } }` nesting; unknown block
14
+ * types are ignored.
11
15
  *
12
16
  * @module dsh-loop-engine/engine-kimi/acp/mapping
13
17
  */
@@ -33,12 +37,37 @@ export declare function chunkDelta(update: AcpUpdate): string;
33
37
  export declare function toolCallIdOf(update: AcpUpdate): string;
34
38
  /** The tool display name (`title`). */
35
39
  export declare function toolCallName(update: AcpUpdate): string;
40
+ /**
41
+ * The tool call's real input as the JSON `arguments` string the durable
42
+ * `tool/call` carries, when the frame supplies it.
43
+ *
44
+ * The `tool_call` announcement never carries `rawInput`; a later
45
+ * `tool_call_update` does (measured against kimi 0.28.x: the execution-start
46
+ * frame, `status: 'in_progress'`). A call's arguments are therefore unknowable
47
+ * at announce time, and the driver logs the call only once an update supplies
48
+ * them — or when the call settles, whichever comes first.
49
+ * @param update - one tool-call announcement or update.
50
+ * @returns the arguments string, or `undefined` when the frame omits the field.
51
+ */
52
+ export declare function toolRawInput(update: AcpToolCallExt | AcpToolCallStreamExt): string | undefined;
36
53
  /** Whether a tool stream status is settled (no longer streaming). */
37
54
  export declare function isToolSettledStatus(status: string): boolean;
38
55
  /** Whether a tool stream status denotes a failure. */
39
56
  export declare function isToolErrorStatus(status: string): boolean;
40
- /** Join the observed `{ type: 'content', content: { type: 'text', text } }` blocks. */
41
- export declare function toolContentText(update: AcpToolCallExt | AcpToolCallStreamExt): string;
57
+ /**
58
+ * The tool call's content as ONE update carries it: the joined text of its
59
+ * observed `{ type: 'content', content: { type: 'text', text } }` blocks.
60
+ *
61
+ * Kimi re-sends the call's whole content on every `tool_call_update` rather than
62
+ * streaming deltas (measured against 0.28.1: the string grows from the tool
63
+ * input's rendering to the final output), so this is a *snapshot* — callers
64
+ * replace with it, never append. `undefined` means the update carried no
65
+ * content field at all (nothing to replace), which is distinct from a present
66
+ * but empty one.
67
+ * @param update - one tool-call announcement or update.
68
+ * @returns the content snapshot, or `undefined` when the field is absent.
69
+ */
70
+ export declare function toolContentText(update: AcpToolCallExt | AcpToolCallStreamExt): string | undefined;
42
71
  /** Project a completed tool call to a durable tool/result message. */
43
72
  export declare function toolResult(callId: string, text: string, isError: boolean): ToolResultMessage;
44
73
  //# sourceMappingURL=mapping.d.ts.map
@@ -33,7 +33,14 @@ export interface AcpUpdate {
33
33
  readonly sessionUpdate: string;
34
34
  readonly [key: string]: unknown;
35
35
  }
36
- /** A tool-call announcement (`sessionUpdate: 'tool_call'`). */
36
+ /**
37
+ * A tool-call announcement (`sessionUpdate: 'tool_call'`).
38
+ *
39
+ * Measured against kimi 0.28.x: an announcement carries the call's identity
40
+ * (`toolCallId`/`title`/`kind`) and an empty content card, but **never** its
41
+ * input — `rawInput` only ever arrives on a later `tool_call_update` (the
42
+ * execution-start frame).
43
+ */
37
44
  export interface AcpToolCallExt extends AcpUpdate {
38
45
  readonly sessionUpdate: 'tool_call';
39
46
  readonly toolCallId: string;
@@ -41,6 +48,8 @@ export interface AcpToolCallExt extends AcpUpdate {
41
48
  readonly kind: string;
42
49
  readonly status: string;
43
50
  readonly content?: readonly AcpToolContentBlock[];
51
+ /** The call's real input; absent on the announcement (see above). */
52
+ readonly rawInput?: unknown;
44
53
  }
45
54
  /** A tool-call progress/result stream (`sessionUpdate: 'tool_call_update'`). */
46
55
  export interface AcpToolCallStreamExt extends AcpUpdate {
@@ -48,11 +57,41 @@ export interface AcpToolCallStreamExt extends AcpUpdate {
48
57
  readonly toolCallId: string;
49
58
  readonly status: string;
50
59
  readonly content?: readonly AcpToolContentBlock[];
60
+ /** The call's real input, first present on the execution-start frame. */
61
+ readonly rawInput?: unknown;
62
+ /** The tool's raw structured output, present once the call settles. */
63
+ readonly rawOutput?: unknown;
64
+ }
65
+ /** One option the agent offers for a `session/request_permission`. */
66
+ export interface AcpPermissionOption {
67
+ /** Opaque id the client sends back in `outcome.optionId` (the only field the agent reads). */
68
+ readonly optionId: string;
69
+ /** Human-readable label for the option. */
70
+ readonly name?: string;
71
+ /** Option class: `allow_once` / `allow_always` / `reject_once` / `reject_always`. */
72
+ readonly kind?: string;
51
73
  }
52
74
  /** The `session/request_permission` reverse-RPC params. */
53
75
  export interface AcpPermissionRequest {
54
76
  readonly sessionId?: string;
55
- readonly request?: unknown;
77
+ /** Every option the agent will accept an answer for; empty means no answerable choice. */
78
+ readonly options?: readonly AcpPermissionOption[];
79
+ /** The tool call the prompt belongs to (presentation/correlation only). */
80
+ readonly toolCall?: unknown;
81
+ }
82
+ /**
83
+ * Terminal outcome of a permission request: the option the client picked, or
84
+ * `cancelled` when it had no answer to give.
85
+ */
86
+ export type AcpPermissionOutcome = {
87
+ readonly outcome: 'selected';
88
+ readonly optionId: string;
89
+ } | {
90
+ readonly outcome: 'cancelled';
91
+ };
92
+ /** The `session/request_permission` result the agent expects (ACP `RequestPermissionResponse`). */
93
+ export interface AcpPermissionResponse {
94
+ readonly outcome: AcpPermissionOutcome;
56
95
  }
57
96
  /** Result of a completed `session/prompt` request (opaque; the turn ended). */
58
97
  export interface AcpPromptResult {
@@ -111,19 +111,76 @@ export declare class KimiAgent implements Agent {
111
111
  private step;
112
112
  /** Per-step accumulation state for streamed assistant blocks and tool calls. */
113
113
  private blocks;
114
- private emittedToolCalls;
115
- private toolText;
114
+ /**
115
+ * Announced calls still waiting for the update that carries their input,
116
+ * keyed by call id, holding the latest content snapshot seen meanwhile (a
117
+ * frame can report output before it reports input). `toolContent` holds the
118
+ * calls already logged, so a call is in exactly one of the two.
119
+ */
120
+ private pendingCalls;
121
+ /** Whether the current step published any assistant message at all. */
122
+ private producedOutput;
123
+ /**
124
+ * Tool results logged into the currently open step. A result means the
125
+ * segment that requested the call is finished, so the next assistant content
126
+ * opens the next step (see {@link beginSegment}).
127
+ */
128
+ private stepSettledTools;
129
+ /**
130
+ * Tool calls the current segment has already received input for, awaiting the
131
+ * segment's single assistant message. A model turn that announces several
132
+ * calls before any result must land them all in ONE message — the chat node
133
+ * keys by `${turn}:${step}` and replaces blocks on every message, so a second
134
+ * message would overwrite the first one's reasoning/text (and tool-call head).
135
+ * The list is flushed once, when the segment closes (its first settled result).
136
+ */
137
+ private segmentCalls;
138
+ /** Latest content snapshot per logged tool call (see {@link applyUpdate}). */
139
+ private toolContent;
140
+ /** The live attempt framing the assistant message being assembled right now. */
141
+ private live;
116
142
  private blockRef;
117
143
  private ensureBlock;
118
- /** Apply one streamed update to the current step's blocks and stream. */
144
+ /**
145
+ * Rotate to the next step when the segment that ran a tool has finished, so
146
+ * each assistant segment lands in its own step.
147
+ *
148
+ * Called as new assistant content begins. A step holding a settled tool
149
+ * result means the previous segment is complete, and the content about to be
150
+ * applied belongs to the next one. Rotating here (rather than when a call is
151
+ * announced) keeps calls that were announced before any result — one model
152
+ * turn — in a single step.
153
+ */
154
+ private beginSegment;
155
+ /** Apply one streamed update to the open step's blocks and stream. */
119
156
  private applyUpdate;
157
+ /** The live attempt framing the current segment, opened on its first chunk. */
158
+ private currentStream;
159
+ /**
160
+ * Flush the open segment's single assistant message and every `tool/call` it
161
+ * accumulated, in the load-bearing order assistant/message → tool/call. One
162
+ * segment produces exactly ONE message even when the model announced several
163
+ * calls before any result, because the chat view keys an assistant node by
164
+ * `${turn}:${step}` and replaces its blocks on every message — a second
165
+ * message in the same step would overwrite the first one's reasoning/text and
166
+ * leave only the last bare tool-call head visible.
167
+ * @param phase - the open step the segment belongs to.
168
+ */
169
+ private flushSegment;
120
170
  /**
121
171
  * Flush the accumulated assistant blocks into one durable assistant/message
122
- * carrying the exact stream the attempt published live.
123
- * @param turn - durable turn owning the message.
124
- * @param step - durable step owning the message.
125
- * @param currentStream - the step's live attempt accessor; a step whose
126
- * blocks streamed always has one open, while a tool-only step has none.
172
+ * carrying the exact stream the attempt published live, optionally closing it
173
+ * with the tool-call block(s) that ended the segment.
174
+ *
175
+ * Every flush settles its own attempt (and so its own live `end` frame): a
176
+ * step emits one message per assistant segment, and the client pairs a durable
177
+ * message with the attempt whose `end` cites it, so two messages may not share
178
+ * one attempt.
179
+ * @param phase - the open step the message belongs to.
180
+ * @param toolCalls - the calls that closed this segment, rendered as trailing
181
+ * `tool-call` content blocks. A segment with no blocks of its own — a step
182
+ * whose only activity was a tool call — still emits this message, so its
183
+ * `tool/call` and `tool/result` events have a parent to pair with.
127
184
  */
128
185
  private flushAssistant;
129
186
  }
@@ -14,12 +14,14 @@
14
14
  *
15
15
  * @module dsh-loop-engine/engine-kimi
16
16
  */
17
- import { Service } from '@deepseek-ai/cordis';
18
17
  import type { Context } from '@deepseek-ai/cordis';
19
18
  import z from '@deepseek-ai/schemastery';
20
- import type { AgentFactory, AgentHandle, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent';
19
+ import type { AgentOptions } from '@deepseek-ai/dsh-agent';
20
+ import type { Session, SessionId } from '@deepseek-ai/dsh-session';
21
+ import { KimiAgent } from './agent.ts';
21
22
  import type { KimiSpawnCapability } from './process.ts';
22
23
  import type { ResolvedConfig } from './types.ts';
24
+ import { HostedLoopFactory } from '../driver-core/hosted-loop-factory.ts';
23
25
  /** Grace in milliseconds for Kimi process-tree termination. */
24
26
  export declare const KIMI_DISPOSE_GRACE_MS = 3000;
25
27
  /** Deployment-owned configuration for the Kimi loop plugin. */
@@ -45,64 +47,13 @@ declare module '@deepseek-ai/cordis' {
45
47
  * transaction: prepare, run setup, then publish through both registries,
46
48
  * announce, and emit `agent/session-start`.
47
49
  */
48
- export declare class KimiLoop extends Service implements AgentFactory {
50
+ export declare class KimiLoop extends HostedLoopFactory<ResolvedConfig, KimiAgent> {
49
51
  /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
50
52
  static inject: string[];
51
- /** Validated configuration owned by the loop plugin. */
52
- readonly config: ResolvedConfig;
53
- private readonly ownership;
54
- /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
55
- private readonly runtime;
56
53
  /** One-shot spawn capability handed to every agent, sandboxed by the subprocess seam. */
57
54
  readonly spawn: KimiSpawnCapability;
58
55
  constructor(ctx: Context, config: Config);
59
- /**
60
- * Construct the driver, scope, and one memoized reverse teardown for a new
61
- * agent. The teardown is registered with the factory and the owner fiber
62
- * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
63
- * fuses caller cancellation with lifecycle teardown for setup awaits.
64
- */
65
- private prepare;
66
- /** Prepare one Agent around an acquired Session, run setup, and publish it. */
67
- private setupAndPublish;
68
- /**
69
- * Create an agent and session under one caller-supplied identity, owned by
70
- * the accessing fiber. When a persistence backend is mounted, the session's
71
- * durable identity is stored before publication.
72
- * @param ownerCtx - caller context that structurally owns the lifecycle.
73
- * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
74
- * @returns the published handle.
75
- */
76
- createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;
77
- /**
78
- * Take a fresh session's write ownership when persistence is mounted.
79
- * Nothing is appended here: the constructor seed (which never re-emits
80
- * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
81
- * publication commit point, so a failed or cancelled setup closes an
82
- * unmaterialized handle and leaves no stored residue — the same id can be
83
- * created again.
84
- * @param session - the unpublished session to store.
85
- * @param signal - optional cancellation forwarded to the backend create.
86
- * @returns the owned handle and stored cursor, or `undefined` without a backend.
87
- */
88
- private createStoredSession;
89
- /**
90
- * Durably store the session events appended since the last stored cursor.
91
- * Pre-publication appends (constructor seed markers, setup-window events)
92
- * never re-emit through `session/event`, so publication must flush them
93
- * through the handle before live events start routing into it.
94
- * @param stored - the session's owned handle and stored cursor, if any.
95
- * @param session - the unpublished session whose suffix is stored.
96
- */
97
- private appendUnstoredSuffix;
98
- /**
99
- * Resume an owned agent from the configured persistence service.
100
- * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
101
- * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
102
- * @returns the published handle.
103
- */
104
- resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;
105
- /** Resume through an explicit persistence service. */
106
- private resumeWith;
56
+ /** Construct the Kimi ACP driver for one prepared session. */
57
+ protected buildAgent(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session): KimiAgent;
107
58
  }
108
59
  //# sourceMappingURL=loop.d.ts.map
@@ -11,6 +11,9 @@
11
11
  * is surfaced under its own name, so the dsh skill-injection seam (`/name`
12
12
  * gestures) can carry them into the prompt.
13
13
  *
14
+ * Unlike pi and Codex, Kimi has no user-level instruction file: its user
15
+ * install root holds only the skills catalog.
16
+ *
14
17
  * The generic `~/.agents/skills/` and `.agents/skills/` roots are deliberately
15
18
  * not scanned here: dsh's own `skill-filesystem` provider already exposes them
16
19
  * through the same registry in the web profile. Kimi built-in Skills are
@@ -21,9 +24,13 @@
21
24
  * `disableModelInvocation`/`type` fields are not translated, so a `type: flow`
22
25
  * skill is surfaced as model-invocable.
23
26
  *
27
+ * The discovery algorithm itself lives in {@link AgentsMdSkillProvider}; this
28
+ * module supplies only Kimi's locations and ranks.
29
+ *
24
30
  * @module dsh-loop-engine/engine-kimi/skills
25
31
  */
26
- import type { SkillCandidate, SkillDefinition, SkillLookupOptions, SkillProvider, SkillProviderControl } from '../skills.ts';
32
+ import { AgentsMdSkillProvider } from '../driver-core/agents-md-skill-provider.ts';
33
+ import type { SkillProviderControl } from '../skills.ts';
27
34
  /**
28
35
  * Resolve the Kimi config directory, honoring the `KIMI_CODE_HOME` environment
29
36
  * override and falling back to `~/.kimi-code`.
@@ -38,20 +45,8 @@ export declare function kimiAgentDir(): string;
38
45
  * - project `.kimi-code/skills/` and user `~/.kimi-code/skills/` — each
39
46
  * `SKILL.md` entry surfaced under its own name.
40
47
  */
41
- export declare class KimiSkillProvider implements SkillProvider {
42
- private readonly control;
43
- readonly name = "kimi";
48
+ export declare class KimiSkillProvider extends AgentsMdSkillProvider {
44
49
  constructor(control: SkillProviderControl);
45
- list(options: SkillLookupOptions): Promise<readonly SkillCandidate[]>;
46
- get(candidate: SkillCandidate, _options: SkillLookupOptions): Promise<SkillDefinition | undefined>;
47
- /** One merged `agents-md` candidate for a ranked file set. */
48
- private agentsCandidate;
49
- /** Collect every skill in one skills directory, both kimi layouts. */
50
- private collectSkillsDir;
51
- /** One parsed skill as a ranked candidate. */
52
- private skillCandidate;
53
- /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
54
- private tryParse;
55
50
  }
56
51
  export default KimiSkillProvider;
57
52
  //# sourceMappingURL=skills.d.ts.map