@multiclawcli/core 0.5.0 → 0.6.0

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.
@@ -1,14 +1,16 @@
1
1
  import type { EventBus } from "../event/EventBus";
2
2
  import type { AgentDefinition, AgentResult } from "../types/agent";
3
3
  import type { MultiClawConfig } from "../types/config";
4
+ import type { AgentSessionManager } from "./AgentSessionManager";
4
5
  import type { ContextManager } from "./ContextManager";
5
6
  export declare class AgentRunner {
6
7
  private eventBus;
7
8
  private contextManager;
8
9
  private globalWorkDir;
9
10
  private config;
10
- constructor(eventBus: EventBus, contextManager: ContextManager, globalWorkDir: string, config: MultiClawConfig);
11
- run(agent: AgentDefinition, signal?: AbortSignal): Promise<AgentResult>;
11
+ private sessionManager?;
12
+ constructor(eventBus: EventBus, contextManager: ContextManager, globalWorkDir: string, config: MultiClawConfig, sessionManager?: AgentSessionManager | undefined);
13
+ run(agent: AgentDefinition, signal?: AbortSignal, instruction?: string): Promise<AgentResult>;
12
14
  /**
13
15
  * Publish one chunk of agent output.
14
16
  *
@@ -0,0 +1,40 @@
1
+ import type { AgentOutput, AgentRuntime, AgentTask } from "../types/runtime";
2
+ /** What one agent did with a mid-turn message. */
3
+ export interface SupplementReport {
4
+ agentId: string;
5
+ /**
6
+ * `injected` landed in the running turn; `idle` had no turn to land in;
7
+ * `unsupported` cannot take mid-turn input at all; `error` failed.
8
+ */
9
+ status: "injected" | "idle" | "unsupported" | "error";
10
+ error?: string;
11
+ }
12
+ /** Owns reusable agent sessions across independent orchestration turns. */
13
+ export declare class AgentSessionManager {
14
+ private entries;
15
+ private resetting?;
16
+ private closing?;
17
+ private closed;
18
+ execute(runtime: AgentRuntime, task: AgentTask): Promise<AgentOutput>;
19
+ /**
20
+ * Deliver a message into every session that is running a turn right now.
21
+ *
22
+ * Only `running` sessions are candidates, and that is the whole safety
23
+ * argument: a session between turns has nothing to inject into, and asking a
24
+ * provider to steer an idle session makes it *start* a turn that this manager
25
+ * never scheduled — one whose output belongs to no turn and which nothing
26
+ * would ever settle. Sessions that are idle are therefore not reported as
27
+ * failures but simply absent, and the caller sees an empty list.
28
+ *
29
+ * One agent failing never stops the others: each report stands alone, because
30
+ * the caller has to be able to say "architect took it, qa did not" rather than
31
+ * "steering failed".
32
+ */
33
+ supplement(text: string): Promise<SupplementReport[]>;
34
+ /** Closes all sessions and permits a fresh conversation after cleanup completes. */
35
+ reset(): Promise<void>;
36
+ /** Terminal, idempotent cleanup barrier. No new executions may start. */
37
+ close(): Promise<void>;
38
+ private drain;
39
+ private closeEntry;
40
+ }
@@ -1,18 +1,21 @@
1
1
  import { EventBus } from "../event/EventBus";
2
2
  import type { MultiClawConfig, OrchestrationPreview, OrchestratorResult } from "../types/config";
3
+ import type { AgentSessionManager } from "./AgentSessionManager";
3
4
  export declare class Orchestrator {
4
5
  private config;
5
6
  readonly eventBus: EventBus;
6
7
  private contextManager;
7
8
  private runner;
8
9
  private workDir;
9
- constructor(config: MultiClawConfig);
10
+ constructor(config: MultiClawConfig, sessions?: AgentSessionManager);
10
11
  preview(options?: {
11
12
  signal?: AbortSignal;
12
13
  }): Promise<OrchestrationPreview>;
13
14
  run(options?: {
14
15
  signal?: AbortSignal;
15
16
  }): Promise<OrchestratorResult>;
17
+ private reviewPolicyError;
18
+ private reviewDelivery;
16
19
  private stageInfos;
17
20
  /**
18
21
  * Order the agents into stages, degrading to declaration order if the graph is
@@ -98,6 +101,7 @@ export declare class Orchestrator {
98
101
  */
99
102
  private finishWithMessage;
100
103
  private warn;
104
+ private finishCancelled;
101
105
  /**
102
106
  * Close out a turn that scheduled no agents.
103
107
  *
@@ -1,17 +1,11 @@
1
- import type { AgentOutput, AgentTask, RuntimeInfo, RuntimeQuery } from "../types/runtime";
1
+ import type { AgentOutput, AgentTask, RuntimeInfo, RuntimeQuery, RuntimeSession, RuntimeSessionOptions } from "../types/runtime";
2
2
  import { RuntimeBase } from "./RuntimeBase";
3
- /**
4
- * Grace period between SIGTERM and SIGKILL when an agent will not stop.
5
- *
6
- * This is time a single attempt can still consume *after* its `timeout`: the
7
- * inner timeout fires a SIGTERM, and a child that ignores it is not reaped until
8
- * this grace elapses — the reject itself lands on the child's `close`. A caller
9
- * that budgets for a whole retry sequence must therefore add this to the
10
- * per-attempt timeout, or its "upper bound" is not one (see `retryBudgetMs`).
11
- */
3
+ /** Grace period between SIGTERM and SIGKILL during process cleanup. */
12
4
  export declare const KILL_ESCALATION_MS = 3000;
5
+ export declare function validateRuntimeQuestionContent(schema: unknown, content: unknown): content is Record<string, string | number | boolean | string[]>;
13
6
  export declare class AcpRuntime extends RuntimeBase {
14
7
  readonly name = "acp";
15
8
  describe(options?: RuntimeQuery): Promise<RuntimeInfo>;
16
9
  execute(task: AgentTask): Promise<AgentOutput>;
10
+ openSession(options: RuntimeSessionOptions): Promise<RuntimeSession>;
17
11
  }
@@ -1,5 +1,6 @@
1
1
  import type { AgentDefinition } from "./agent";
2
2
  import type { LeaderMessage } from "./leader";
3
+ import type { RuntimePermissionHandler, RuntimeQuestionHandler } from "./runtime";
3
4
  /**
4
5
  * One finished turn of a session, as dialogue the leader can read back.
5
6
  *
@@ -14,6 +15,26 @@ export interface TurnRecord {
14
15
  /** How the turn ended in words, when it ended that way. */
15
16
  reply?: LeaderMessage;
16
17
  }
18
+ export interface ReviewPolicy {
19
+ /** Configured agent reserved for final review, outside the normal pipeline. */
20
+ reviewer: string;
21
+ /** Configured workers allowed to repair, in dependency order. */
22
+ repairAgents: string[];
23
+ /** Maximum repair passes (0–5); defaults to 1. Review runs once more after each pass. */
24
+ maxRepairRounds?: number;
25
+ }
26
+ export interface ReviewVerdict {
27
+ verdict: "approved" | "changes_required";
28
+ summary: string;
29
+ issues: string[];
30
+ }
31
+ export interface AcceptanceResult {
32
+ status: "approved" | "changes_required" | "failed" | "cancelled";
33
+ reviewer: string;
34
+ repairRounds: number;
35
+ reviews: ReviewVerdict[];
36
+ reason?: string;
37
+ }
17
38
  export interface MultiClawConfig {
18
39
  name: string;
19
40
  workDir: string;
@@ -48,6 +69,17 @@ export interface MultiClawConfig {
48
69
  continueOnError?: boolean;
49
70
  maxConcurrency?: number;
50
71
  useLeader?: boolean;
72
+ /** Opt-in delivery gate. Execution success alone does not satisfy this policy. */
73
+ review?: ReviewPolicy;
74
+ /** Caller-owned live approvals. Ask mode without a handler fails closed. */
75
+ permissions?: {
76
+ mode: "auto" | "ask" | "deny";
77
+ onRequest?: RuntimePermissionHandler;
78
+ };
79
+ /** Caller-owned structured questions; headless callers cancel explicitly. */
80
+ questions?: {
81
+ onRequest: RuntimeQuestionHandler;
82
+ };
51
83
  /**
52
84
  * Project-level defaults applied to every agent that does not override them.
53
85
  * Keeps generated configs readable: one line instead of 12 repeated parameters.
@@ -77,7 +109,10 @@ export interface DeliverableSummary {
77
109
  }
78
110
  export interface OrchestratorResult {
79
111
  name: string;
112
+ /** With review enabled, true only when execution and delivery acceptance both succeed. */
80
113
  success: boolean;
114
+ /** Present only for a review-gated pipeline, never for a leader reply/ask. */
115
+ acceptance?: AcceptanceResult;
81
116
  totalDuration: number;
82
117
  agentResults: import("./agent").AgentResult[];
83
118
  startTime: string;
@@ -10,6 +10,8 @@ export type LeaderMode = "run" | "reply" | "ask";
10
10
  export type LeaderDecision = {
11
11
  mode: "run";
12
12
  run: string[];
13
+ /** Complete task combining the original goal with clarification constraints. */
14
+ requirement?: string;
13
15
  /**
14
16
  * Why these agents, in the leader's own words.
15
17
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Everything a runtime emits about a run carries the same envelope. `seq` is a
3
3
  * per-run monotonic counter so a consumer can order events without trusting the
4
- * clock; `runId` is fresh for every `execute()` call.
4
+ * clock; `runId` is fresh for every `execute()` call or session `prompt()` turn.
5
5
  */
6
6
  export interface RuntimeEventBase {
7
7
  runId: string;
@@ -94,6 +94,38 @@ export interface RuntimeQuery {
94
94
  /** Binary this runtime would launch, when the caller knows it. */
95
95
  command?: string;
96
96
  }
97
+ export interface RuntimePermissionRequest extends RuntimeEventBase {
98
+ requestId: string;
99
+ sessionId: string;
100
+ toolCallId: string;
101
+ title: string;
102
+ toolName?: string;
103
+ kind?: string;
104
+ input?: unknown;
105
+ /** Only choices permitted by the configured tools policy are offered. */
106
+ options: Array<{
107
+ optionId: string;
108
+ name: string;
109
+ kind: "allow_once" | "allow_always" | "reject_once" | "reject_always";
110
+ }>;
111
+ }
112
+ /** Return an offered option id, or undefined to cancel/deny. Never grant outside the tools policy. */
113
+ export type RuntimePermissionHandler = (request: RuntimePermissionRequest, signal: AbortSignal) => Promise<string | undefined>;
114
+ export interface RuntimeQuestionRequest extends RuntimeEventBase {
115
+ requestId: string;
116
+ sessionId: string;
117
+ toolCallId?: string;
118
+ message: string;
119
+ requestedSchema: import("@agentclientprotocol/sdk").ElicitationSchema;
120
+ }
121
+ export type RuntimeQuestionResponse = {
122
+ action: "accept";
123
+ content: Record<string, string | number | boolean | string[]>;
124
+ } | {
125
+ action: "decline" | "cancel";
126
+ };
127
+ /** Structured form input only; the signal aborts when the requesting turn ends. */
128
+ export type RuntimeQuestionHandler = (request: RuntimeQuestionRequest, signal: AbortSignal) => Promise<RuntimeQuestionResponse>;
97
129
  export interface AgentTask {
98
130
  agentId: string;
99
131
  systemPrompt: string;
@@ -103,6 +135,10 @@ export interface AgentTask {
103
135
  timeout: number;
104
136
  signal?: AbortSignal;
105
137
  onEvent?: (event: RuntimeEvent) => void;
138
+ /** Automatic tools policy by default; ask without a handler denies. */
139
+ permissionMode?: "auto" | "ask" | "deny";
140
+ onPermission?: RuntimePermissionHandler;
141
+ onQuestion?: RuntimeQuestionHandler;
106
142
  /**
107
143
  * Non-fatal diagnostics the runtime wants the caller to surface.
108
144
  *
@@ -139,8 +175,83 @@ export interface AgentOutput {
139
175
  costUsd?: number;
140
176
  stopReason?: string;
141
177
  }
178
+ /** Immutable agent configuration shared by all turns in an open session. */
179
+ export type RuntimeSessionOptions = Omit<AgentTask, "prompt" | "onEvent" | "signal" | "timeout" | "permissionMode" | "onPermission" | "onQuestion"> & {
180
+ /** Immutable negotiated form capability; callbacks remain turn-local. */
181
+ questionForms?: boolean;
182
+ /** Lifetime cancellation, including startup and idle time; closes the session. */
183
+ signal?: AbortSignal;
184
+ /** Startup deadline and default per-turn timeout in ms (ACP defaults to 600,000). */
185
+ timeout?: number;
186
+ };
187
+ export interface RuntimePromptOptions {
188
+ /** Cancels only this turn. A cooperative provider leaves the session reusable. */
189
+ signal?: AbortSignal;
190
+ /** Overrides the session's default per-turn deadline. */
191
+ timeout?: number;
192
+ /** Turn-local events, with a fresh runId and sequence counter. */
193
+ onEvent?: (event: RuntimeEvent) => void;
194
+ /** Waiting uses the existing turn timeout; it does not pause the deadline. */
195
+ permissionMode?: "auto" | "ask" | "deny";
196
+ onPermission?: RuntimePermissionHandler;
197
+ onQuestion?: RuntimeQuestionHandler;
198
+ }
199
+ /**
200
+ * What a mid-turn message did.
201
+ *
202
+ * `injected` means the running turn took it — the agent will see it before it
203
+ * finishes. `idle` means no turn was running, so nothing was applied and the
204
+ * caller still owns the text; deciding to queue it is the caller's job, not the
205
+ * runtime's.
206
+ */
207
+ export type RuntimeSupplementResult = {
208
+ status: "injected";
209
+ } | {
210
+ status: "idle";
211
+ };
212
+ /** Caller-owned live session. Always await close() in a finally block. */
213
+ export interface RuntimeSession {
214
+ readonly sessionId: string;
215
+ readonly state: "idle" | "running" | "cancelling" | "closing" | "closed";
216
+ /**
217
+ * What this session can do beyond a single turn.
218
+ *
219
+ * `supplement` is negotiated with the provider rather than assumed: it is true
220
+ * only when the agent advertised mid-turn input during `initialize` (ACP
221
+ * `_meta.steering.supported`), so a bridge that does not implement it is
222
+ * reported as unable instead of being sent a method it will ignore.
223
+ */
224
+ readonly capabilities: {
225
+ continuation: boolean;
226
+ supplement: boolean;
227
+ };
228
+ /** Rejects overlapping turns; it does not implicitly queue or inject input. */
229
+ prompt(text: string, options?: RuntimePromptOptions): Promise<AgentOutput>;
230
+ /**
231
+ * Delivers a message into the turn that is running *now*, when
232
+ * {@link RuntimeSession.capabilities} allows it.
233
+ *
234
+ * Only meaningful mid-turn. An idle session resolves `idle` and must not start
235
+ * a turn from it: `prompt` is how a turn starts, and a turn the caller never
236
+ * started has no owner to settle it, cancel it or show its output. Rejects on
237
+ * provider errors — "did it land?" must never be a guess.
238
+ */
239
+ supplement?(text: string, options?: {
240
+ signal?: AbortSignal;
241
+ }): Promise<RuntimeSupplementResult>;
242
+ /**
243
+ * Idempotently cancels the current turn and waits for its terminal response.
244
+ * ACP closes uncooperative providers after a grace period; cleanup failure rejects.
245
+ * Idle cancellation is a no-op. The prompt may reject independently.
246
+ */
247
+ cancelTurn(): Promise<void>;
248
+ /** Idempotent cleanup barrier; rejects if process cleanup cannot be acknowledged. */
249
+ close(): Promise<void>;
250
+ }
142
251
  export interface AgentRuntime {
143
252
  readonly name: string;
144
253
  describe(options?: RuntimeQuery): Promise<RuntimeInfo>;
145
254
  execute(task: AgentTask): Promise<AgentOutput>;
255
+ /** Optional live-session API; existing execute-only runtimes remain compatible. */
256
+ openSession?(options: RuntimeSessionOptions): Promise<RuntimeSession>;
146
257
  }
@@ -1 +1,3 @@
1
- export declare function withTimeout<T>(factory: (signal: AbortSignal) => Promise<T>, ms: number, label: string, signal?: AbortSignal): Promise<T>;
1
+ export declare function withTimeout<T>(factory: (signal: AbortSignal) => Promise<T>, ms: number, label: string, signal?: AbortSignal, options?: {
2
+ waitForCleanup?: boolean;
3
+ }): Promise<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiclawcli/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,9 +26,9 @@
26
26
  "vitest": "^5.0.4"
27
27
  },
28
28
  "dependencies": {
29
- "@agentclientprotocol/claude-agent-acp": "^0.93.0",
29
+ "@agentclientprotocol/claude-agent-acp": "^0.95.0",
30
30
  "@agentclientprotocol/codex-acp": "^2.2.2",
31
- "@agentclientprotocol/sdk": "^1.9.0"
31
+ "@agentclientprotocol/sdk": "^2.0.2"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",