@aionis/openclaw-adapter 0.1.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.
@@ -0,0 +1,347 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { DEFAULT_THRESHOLDS } from "../types/config.js";
3
+ import { classifyBroadScan, classifyBroadTest, inferProgress, summarizeToolResult } from "./heuristics.js";
4
+ import { createRunState, hashStable, LoopStateStore } from "./state.js";
5
+ export class AionisLoopControlAdapter {
6
+ client;
7
+ config;
8
+ states = new LoopStateStore();
9
+ constructor(client, config) {
10
+ this.client = client;
11
+ const mergedThresholds = { ...DEFAULT_THRESHOLDS, ...(config.thresholds ?? {}) };
12
+ this.config = {
13
+ ...config,
14
+ thresholds: mergedThresholds,
15
+ strictToolBlocking: config.strictToolBlocking ?? true,
16
+ replayDispatchEnabled: config.replayDispatchEnabled ?? true,
17
+ handoffFallbackEnabled: config.handoffFallbackEnabled ?? true,
18
+ };
19
+ }
20
+ sessionStart(ctx) {
21
+ const scope = this.config.scopeResolver(ctx);
22
+ const state = createRunState({
23
+ stateId: ctx.sessionId,
24
+ scope,
25
+ agentId: ctx.agentId,
26
+ sessionKey: ctx.sessionKey,
27
+ sessionId: ctx.sessionId,
28
+ workspaceDir: ctx.workspaceDir,
29
+ });
30
+ this.states.upsert(state);
31
+ return this.states.link(state, ctx.sessionId, ctx.sessionKey);
32
+ }
33
+ sessionEnd(sessionId) {
34
+ const state = this.states.get(sessionId);
35
+ if (state) {
36
+ this.states.deleteAllFor(state);
37
+ return;
38
+ }
39
+ this.states.delete(sessionId);
40
+ }
41
+ async beforeAgentStart(event, ctx) {
42
+ const state = this.ensureState({
43
+ prompt: event.prompt,
44
+ agentId: ctx.agentId,
45
+ sessionKey: ctx.sessionKey,
46
+ sessionId: ctx.sessionId,
47
+ workspaceDir: ctx.workspaceDir,
48
+ });
49
+ if (!this.client.contextAssemble)
50
+ return undefined;
51
+ const candidates = this.extractCandidateTools(event.messages);
52
+ const out = await this.client.contextAssemble({
53
+ scope: state.scope,
54
+ queryText: event.prompt,
55
+ context: {
56
+ source: "openclaw-adapter.before_agent_start",
57
+ agent_id: ctx.agentId,
58
+ session_key: ctx.sessionKey,
59
+ session_id: ctx.sessionId,
60
+ trigger: ctx.trigger,
61
+ },
62
+ toolCandidates: candidates,
63
+ });
64
+ const merged = out?.layered_context?.merged_text?.trim();
65
+ const decision = out?.tools ?? undefined;
66
+ this.captureDecision(state, decision ?? undefined);
67
+ if (!merged)
68
+ return undefined;
69
+ return { prependContext: `<aionis-context>\n${merged}\n</aionis-context>` };
70
+ }
71
+ async beforeToolCall(event, ctx) {
72
+ const state = this.ensureState({
73
+ agentId: ctx.agentId,
74
+ sessionKey: ctx.sessionKey,
75
+ sessionId: ctx.sessionId,
76
+ runId: event.runId ?? ctx.runId,
77
+ workspaceDir: ctx.workspaceDir,
78
+ });
79
+ const currentToolHash = hashStable(event.params);
80
+ state.stepCount += 1;
81
+ state.sameToolStreak = state.lastToolName === event.toolName && state.lastToolParamsHash === currentToolHash
82
+ ? state.sameToolStreak + 1
83
+ : 1;
84
+ state.lastToolName = event.toolName;
85
+ state.lastToolParamsHash = currentToolHash;
86
+ if (classifyBroadScan(event.toolName, event.params))
87
+ state.broadScanCount += 1;
88
+ if (classifyBroadTest(event.toolName, event.params))
89
+ state.broadTestCount += 1;
90
+ const thresholdStop = this.checkThresholds(state);
91
+ if (thresholdStop) {
92
+ return this.makeStopResult(state, thresholdStop, event, ctx);
93
+ }
94
+ const candidates = [event.toolName];
95
+ const context = {
96
+ source: "openclaw-adapter.before_tool_call",
97
+ agent_id: ctx.agentId,
98
+ session_key: ctx.sessionKey,
99
+ session_id: ctx.sessionId,
100
+ run_id: event.runId ?? ctx.runId,
101
+ tool_name: event.toolName,
102
+ same_tool_streak: state.sameToolStreak,
103
+ duplicate_observation_streak: state.duplicateObservationStreak,
104
+ no_progress_streak: state.noProgressStreak,
105
+ broad_test_count: state.broadTestCount,
106
+ broad_scan_count: state.broadScanCount,
107
+ estimated_token_burn: state.estimatedTokenBurn,
108
+ };
109
+ if (this.client.rulesEvaluate) {
110
+ await this.client.rulesEvaluate({
111
+ scope: state.scope,
112
+ context,
113
+ candidates,
114
+ });
115
+ }
116
+ const decision = this.client.toolsSelect
117
+ ? await this.client.toolsSelect({
118
+ scope: state.scope,
119
+ runId: event.runId ?? ctx.runId ?? state.stateId,
120
+ context,
121
+ candidates,
122
+ })
123
+ : undefined;
124
+ this.captureDecision(state, decision ?? undefined);
125
+ if (decision?.selected_tool && decision.selected_tool !== event.toolName && this.config.strictToolBlocking) {
126
+ return {
127
+ block: true,
128
+ blockReason: `policy selected ${decision.selected_tool} instead of ${event.toolName}`,
129
+ };
130
+ }
131
+ return undefined;
132
+ }
133
+ async afterToolCall(event, ctx) {
134
+ const state = this.ensureState({
135
+ agentId: ctx.agentId,
136
+ sessionKey: ctx.sessionKey,
137
+ sessionId: ctx.sessionId,
138
+ runId: event.runId ?? ctx.runId,
139
+ workspaceDir: ctx.workspaceDir,
140
+ });
141
+ const summary = summarizeToolResult(event.result, event.error);
142
+ const observationHash = createHash("sha1").update(summary).digest("hex");
143
+ state.duplicateObservationStreak = state.lastObservationHash === observationHash
144
+ ? state.duplicateObservationStreak + 1
145
+ : 0;
146
+ state.lastObservationHash = observationHash;
147
+ const progress = inferProgress(event.toolName, summary);
148
+ state.noProgressStreak = progress ? 0 : state.noProgressStreak + 1;
149
+ state.estimatedLatencyBurnMs += Number(event.durationMs ?? 0);
150
+ state.estimatedTokenBurn += Math.max(1, Math.ceil(summary.length / 4));
151
+ if (this.client.toolsFeedback) {
152
+ await this.client.toolsFeedback({
153
+ scope: state.scope,
154
+ runId: event.runId ?? ctx.runId,
155
+ decisionId: state.lastDecisionId,
156
+ decisionUri: state.lastDecisionUri,
157
+ context: {
158
+ source: "openclaw-adapter.after_tool_call",
159
+ progress,
160
+ duration_ms: event.durationMs ?? null,
161
+ error: event.error ?? null,
162
+ },
163
+ candidates: [event.toolName],
164
+ selectedTool: event.toolName,
165
+ outcome: event.error ? "negative" : progress ? "positive" : "neutral",
166
+ note: progress ? "tool call made progress" : "tool call made no clear progress",
167
+ inputText: summary,
168
+ });
169
+ }
170
+ if (this.client.write) {
171
+ await this.client.write({
172
+ scope: state.scope,
173
+ inputText: `${event.toolName}: ${summary}`,
174
+ metadata: {
175
+ source: "openclaw-adapter.after_tool_call",
176
+ run_id: event.runId ?? ctx.runId,
177
+ tool_call_id: event.toolCallId ?? ctx.toolCallId,
178
+ duration_ms: event.durationMs ?? null,
179
+ },
180
+ });
181
+ }
182
+ }
183
+ async agentEnd(event, ctx) {
184
+ const state = this.findState({ sessionId: ctx.sessionId, sessionKey: ctx.sessionKey }) ?? this.ensureState({
185
+ agentId: ctx.agentId,
186
+ sessionKey: ctx.sessionKey,
187
+ sessionId: ctx.sessionId,
188
+ workspaceDir: ctx.workspaceDir,
189
+ });
190
+ if (!event.success && this.config.handoffFallbackEnabled && this.client.handoffStore && !state.handoffTriggered) {
191
+ state.handoffTriggered = true;
192
+ await this.client.handoffStore({
193
+ scope: state.scope,
194
+ anchor: `openclaw-loop-${state.stateId}`,
195
+ filePath: ctx.workspaceDir ?? "workspace",
196
+ summary: `OpenClaw run stopped: ${state.forcedStopReason ?? "agent_end_failure"}`,
197
+ handoffText: `Resume from degraded run. reason=${state.forcedStopReason ?? "agent_end_failure"}`,
198
+ repoRoot: ctx.workspaceDir ?? null,
199
+ handoffKind: "task_handoff",
200
+ title: "OpenClaw degraded run handoff",
201
+ risk: event.error ?? null,
202
+ });
203
+ }
204
+ }
205
+ decorateStopMessage(message, reason) {
206
+ if (!reason)
207
+ return message;
208
+ return {
209
+ ...message,
210
+ aionis_loop_control: {
211
+ reason,
212
+ },
213
+ };
214
+ }
215
+ ensureState(args) {
216
+ const stateId = args.runId ?? args.sessionId ?? args.sessionKey ?? randomUUID();
217
+ const existing = this.states.get(stateId);
218
+ if (existing)
219
+ return existing;
220
+ if (args.runId) {
221
+ const existingBySession = this.findState({ sessionId: args.sessionId, sessionKey: args.sessionKey });
222
+ if (existingBySession) {
223
+ existingBySession.runId = args.runId;
224
+ existingBySession.workspaceDir = args.workspaceDir ?? existingBySession.workspaceDir;
225
+ this.states.link(existingBySession, args.runId, args.sessionId, args.sessionKey);
226
+ return existingBySession;
227
+ }
228
+ }
229
+ const scope = this.config.scopeResolver(args);
230
+ const created = this.states.upsert(createRunState({
231
+ stateId,
232
+ scope,
233
+ agentId: args.agentId,
234
+ sessionKey: args.sessionKey,
235
+ sessionId: args.sessionId,
236
+ runId: args.runId,
237
+ workspaceDir: args.workspaceDir,
238
+ prompt: args.prompt,
239
+ }));
240
+ return this.states.link(created, args.runId, args.sessionId, args.sessionKey);
241
+ }
242
+ findState(args) {
243
+ if (args.sessionId) {
244
+ const bySession = this.states.get(args.sessionId);
245
+ if (bySession)
246
+ return bySession;
247
+ }
248
+ if (args.sessionKey) {
249
+ return this.states.get(args.sessionKey);
250
+ }
251
+ return undefined;
252
+ }
253
+ captureDecision(state, decision) {
254
+ if (!decision)
255
+ return;
256
+ state.lastDecisionId = decision.decision_id ?? state.lastDecisionId;
257
+ state.lastDecisionUri = decision.decision_uri ?? state.lastDecisionUri;
258
+ state.lastSelectedTool = decision.selected_tool ?? decision.selected ?? state.lastSelectedTool;
259
+ }
260
+ extractCandidateTools(messages) {
261
+ const out = new Set();
262
+ for (const message of messages ?? []) {
263
+ if (!message || typeof message !== "object")
264
+ continue;
265
+ const record = message;
266
+ const toolName = record.toolName ?? record.name;
267
+ if (typeof toolName === "string" && toolName.trim())
268
+ out.add(toolName.trim());
269
+ }
270
+ return [...out];
271
+ }
272
+ resolveReplayHint(state, event, ctx) {
273
+ return this.config.replayHintResolver?.({
274
+ agentId: ctx.agentId,
275
+ sessionKey: ctx.sessionKey,
276
+ sessionId: ctx.sessionId,
277
+ runId: event.runId ?? ctx.runId,
278
+ workspaceDir: ctx.workspaceDir,
279
+ toolName: event.toolName,
280
+ toolParams: event.params,
281
+ }) ?? undefined;
282
+ }
283
+ checkThresholds(state) {
284
+ const t = this.config.thresholds;
285
+ if (state.stepCount > t.maxSteps)
286
+ return "max_steps_exceeded";
287
+ if (state.sameToolStreak > t.maxSameToolStreak)
288
+ return "same_tool_streak_exceeded";
289
+ if (state.duplicateObservationStreak > t.maxDuplicateObservationStreak)
290
+ return "duplicate_observation_exceeded";
291
+ if (state.noProgressStreak > t.maxNoProgressStreak)
292
+ return "no_progress_exceeded";
293
+ if (state.estimatedTokenBurn > t.maxEstimatedTokenBurn)
294
+ return "budget_exceeded";
295
+ if (state.broadTestCount > t.maxBroadTestInvocations)
296
+ return "policy_denied_only_path";
297
+ if (state.broadScanCount > t.maxBroadScanInvocations)
298
+ return "policy_denied_only_path";
299
+ return undefined;
300
+ }
301
+ async makeStopResult(state, reason, event, ctx) {
302
+ state.forcedStopReason = reason;
303
+ const replayHint = this.resolveReplayHint(state, event, ctx);
304
+ if (replayHint && this.config.replayDispatchEnabled && this.client.replayPlaybookCandidate && this.client.replayPlaybookDispatch && !state.replayDispatchAttempted) {
305
+ state.replayDispatchAttempted = true;
306
+ const candidateResult = await this.client.replayPlaybookCandidate({
307
+ scope: state.scope,
308
+ playbookId: replayHint.playbookId,
309
+ version: replayHint.version,
310
+ deterministicGate: replayHint.deterministicGate,
311
+ });
312
+ if (candidateResult?.candidate?.eligible_for_deterministic_replay) {
313
+ await this.client.replayPlaybookDispatch({
314
+ scope: state.scope,
315
+ playbookId: replayHint.playbookId,
316
+ version: replayHint.version,
317
+ params: {
318
+ source: "openclaw-adapter.loop-control",
319
+ reason,
320
+ ...(replayHint.params ?? {}),
321
+ },
322
+ mode: replayHint.mode ?? candidateResult.candidate.recommended_mode,
323
+ maxSteps: replayHint.maxSteps,
324
+ deterministicGate: replayHint.deterministicGate,
325
+ });
326
+ state.forcedStopReason = "replay_dispatch_selected";
327
+ return { block: true, blockReason: "replay dispatch selected" };
328
+ }
329
+ }
330
+ if (this.config.handoffFallbackEnabled && this.client.handoffStore && !state.handoffTriggered) {
331
+ state.handoffTriggered = true;
332
+ await this.client.handoffStore({
333
+ scope: state.scope,
334
+ anchor: `openclaw-loop-${state.stateId}`,
335
+ filePath: ctx.workspaceDir ?? "workspace",
336
+ summary: `Forced loop stop: ${reason}`,
337
+ handoffText: `The run was stopped before ${event.toolName}. reason=${reason}. Resume from current workspace state with a narrower tool path.`,
338
+ repoRoot: ctx.workspaceDir ?? null,
339
+ handoffKind: "task_handoff",
340
+ title: "OpenClaw loop-control handoff",
341
+ });
342
+ state.forcedStopReason = "handoff_store_selected";
343
+ return { block: true, blockReason: "handoff stored after loop-control stop" };
344
+ }
345
+ return { block: true, blockReason: reason };
346
+ }
347
+ }
@@ -0,0 +1,47 @@
1
+ import type { LoopStopReasonCode } from "../types/config.js";
2
+ export type LoopRunState = {
3
+ stateId: string;
4
+ agentId?: string;
5
+ sessionKey?: string;
6
+ sessionId?: string;
7
+ runId?: string;
8
+ workspaceDir?: string;
9
+ scope: string;
10
+ promptHash?: string;
11
+ stepCount: number;
12
+ sameToolStreak: number;
13
+ duplicateObservationStreak: number;
14
+ noProgressStreak: number;
15
+ broadTestCount: number;
16
+ broadScanCount: number;
17
+ estimatedTokenBurn: number;
18
+ estimatedLatencyBurnMs: number;
19
+ lastToolName?: string;
20
+ lastToolParamsHash?: string;
21
+ lastObservationHash?: string;
22
+ lastDecisionId?: string;
23
+ lastDecisionUri?: string;
24
+ lastSelectedTool?: string;
25
+ forcedStopReason?: LoopStopReasonCode;
26
+ handoffTriggered: boolean;
27
+ replayDispatchAttempted: boolean;
28
+ };
29
+ export declare function hashStable(value: unknown): string;
30
+ export declare function createRunState(args: {
31
+ stateId: string;
32
+ scope: string;
33
+ agentId?: string;
34
+ sessionKey?: string;
35
+ sessionId?: string;
36
+ runId?: string;
37
+ workspaceDir?: string;
38
+ prompt?: string;
39
+ }): LoopRunState;
40
+ export declare class LoopStateStore {
41
+ private readonly states;
42
+ get(stateId: string): LoopRunState | undefined;
43
+ upsert(state: LoopRunState): LoopRunState;
44
+ link(state: LoopRunState, ...ids: Array<string | undefined>): LoopRunState;
45
+ delete(stateId: string): void;
46
+ deleteAllFor(target: LoopRunState): void;
47
+ }
@@ -0,0 +1,53 @@
1
+ import { createHash } from "node:crypto";
2
+ export function hashStable(value) {
3
+ return createHash("sha1").update(JSON.stringify(value ?? null)).digest("hex");
4
+ }
5
+ export function createRunState(args) {
6
+ return {
7
+ stateId: args.stateId,
8
+ agentId: args.agentId,
9
+ sessionKey: args.sessionKey,
10
+ sessionId: args.sessionId,
11
+ runId: args.runId,
12
+ workspaceDir: args.workspaceDir,
13
+ scope: args.scope,
14
+ promptHash: args.prompt ? hashStable(args.prompt) : undefined,
15
+ stepCount: 0,
16
+ sameToolStreak: 0,
17
+ duplicateObservationStreak: 0,
18
+ noProgressStreak: 0,
19
+ broadTestCount: 0,
20
+ broadScanCount: 0,
21
+ estimatedTokenBurn: 0,
22
+ estimatedLatencyBurnMs: 0,
23
+ handoffTriggered: false,
24
+ replayDispatchAttempted: false,
25
+ };
26
+ }
27
+ export class LoopStateStore {
28
+ states = new Map();
29
+ get(stateId) {
30
+ return this.states.get(stateId);
31
+ }
32
+ upsert(state) {
33
+ this.states.set(state.stateId, state);
34
+ return state;
35
+ }
36
+ link(state, ...ids) {
37
+ for (const id of ids) {
38
+ if (!id)
39
+ continue;
40
+ this.states.set(id, state);
41
+ }
42
+ return state;
43
+ }
44
+ delete(stateId) {
45
+ this.states.delete(stateId);
46
+ }
47
+ deleteAllFor(target) {
48
+ for (const [key, value] of this.states.entries()) {
49
+ if (value === target)
50
+ this.states.delete(key);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ import type { OpenClawHostApi } from "../types/openclaw.js";
2
+ import { AionisLoopControlAdapter } from "../adapter/loop-control-adapter.js";
3
+ export declare function attachToOpenClawHost(api: OpenClawHostApi, adapter: AionisLoopControlAdapter): void;
@@ -0,0 +1,36 @@
1
+ export function attachToOpenClawHost(api, adapter) {
2
+ if (typeof api.on !== "function") {
3
+ api.logger.warn("openclaw-aionis-adapter: host hook API unavailable; binding skipped");
4
+ return;
5
+ }
6
+ api.on("session_start", async (event, ctx) => {
7
+ adapter.sessionStart({ ...ctx, sessionId: event.sessionId });
8
+ });
9
+ api.on("session_end", async (event) => {
10
+ adapter.sessionEnd(event.sessionId);
11
+ });
12
+ api.on("before_agent_start", async (event, ctx) => {
13
+ return adapter.beforeAgentStart(event, ctx);
14
+ });
15
+ api.on("before_tool_call", async (event, ctx) => {
16
+ return adapter.beforeToolCall(event, ctx);
17
+ });
18
+ api.on("after_tool_call", async (event, ctx) => {
19
+ await adapter.afterToolCall(event, ctx);
20
+ });
21
+ api.on("agent_end", async (event, ctx) => {
22
+ await adapter.agentEnd(event, ctx);
23
+ });
24
+ api.on("tool_result_persist", async (event) => {
25
+ const reason = typeof event.message?.aionis_loop_control === "object"
26
+ ? event.message.aionis_loop_control.reason
27
+ : undefined;
28
+ return { message: adapter.decorateStopMessage(event.message, reason) };
29
+ });
30
+ api.on("before_message_write", async (event) => {
31
+ const reason = typeof event.message?.aionis_loop_control === "object"
32
+ ? event.message.aionis_loop_control.reason
33
+ : undefined;
34
+ return { message: adapter.decorateStopMessage(event.message, reason) };
35
+ });
36
+ }
@@ -0,0 +1,108 @@
1
+ import type { AionisHttpClientOptions, AionisLoopControlClient, AionisToolDecision } from "../types/aionis.js";
2
+ export declare class AionisHttpClientError extends Error {
3
+ readonly status: number;
4
+ readonly code?: string | undefined;
5
+ readonly details?: unknown | undefined;
6
+ constructor(message: string, status: number, code?: string | undefined, details?: unknown | undefined);
7
+ }
8
+ export declare class AionisHttpLoopControlClient implements AionisLoopControlClient {
9
+ private readonly options;
10
+ constructor(options: AionisHttpClientOptions);
11
+ private post;
12
+ private withIdentity;
13
+ contextAssemble(args: {
14
+ scope: string;
15
+ queryText: string;
16
+ context: Record<string, unknown>;
17
+ toolCandidates?: string[];
18
+ }): Promise<{
19
+ layered_context?: {
20
+ merged_text?: string;
21
+ };
22
+ tools?: AionisToolDecision;
23
+ } | null>;
24
+ rulesEvaluate(args: {
25
+ scope: string;
26
+ context: Record<string, unknown>;
27
+ candidates: string[];
28
+ }): Promise<Record<string, unknown> | null>;
29
+ toolsSelect(args: {
30
+ scope: string;
31
+ runId: string;
32
+ context: Record<string, unknown>;
33
+ candidates: string[];
34
+ }): Promise<AionisToolDecision | null>;
35
+ toolsDecision(args: {
36
+ scope: string;
37
+ decisionId?: string;
38
+ decisionUri?: string;
39
+ runId?: string;
40
+ }): Promise<Record<string, unknown> | null>;
41
+ toolsFeedback(args: {
42
+ scope: string;
43
+ runId?: string;
44
+ decisionId?: string;
45
+ decisionUri?: string;
46
+ context: Record<string, unknown>;
47
+ candidates: string[];
48
+ selectedTool: string;
49
+ outcome: "positive" | "negative" | "neutral";
50
+ note?: string;
51
+ inputText: string;
52
+ }): Promise<Record<string, unknown> | null>;
53
+ write(args: {
54
+ scope: string;
55
+ inputText: string;
56
+ metadata?: Record<string, unknown>;
57
+ }): Promise<Record<string, unknown> | null>;
58
+ handoffStore(args: {
59
+ scope: string;
60
+ anchor: string;
61
+ filePath: string;
62
+ summary: string;
63
+ handoffText: string;
64
+ repoRoot?: string | null;
65
+ symbol?: string | null;
66
+ handoffKind?: "patch_handoff" | "review_handoff" | "task_handoff";
67
+ title?: string | null;
68
+ risk?: string | null;
69
+ acceptanceChecks?: string[];
70
+ tags?: string[];
71
+ targetFiles?: string[];
72
+ nextAction?: string | null;
73
+ mustChange?: string[];
74
+ mustRemove?: string[];
75
+ mustKeep?: string[];
76
+ }): Promise<Record<string, unknown> | null>;
77
+ replayPlaybookCandidate(args: {
78
+ scope: string;
79
+ playbookId: string;
80
+ version?: number;
81
+ deterministicGate?: Record<string, unknown>;
82
+ }): Promise<{
83
+ playbook?: {
84
+ playbook_id?: string;
85
+ version?: number;
86
+ status?: string;
87
+ name?: string | null;
88
+ uri?: string;
89
+ };
90
+ candidate?: {
91
+ eligible_for_deterministic_replay?: boolean;
92
+ recommended_mode?: "simulate" | "strict" | "guided";
93
+ next_action?: string;
94
+ mismatch_reasons?: string[];
95
+ };
96
+ cost_signals?: Record<string, unknown>;
97
+ } | null>;
98
+ replayPlaybookDispatch(args: {
99
+ scope: string;
100
+ playbookId: string;
101
+ version?: number;
102
+ params?: Record<string, unknown>;
103
+ mode?: "simulate" | "strict" | "guided";
104
+ maxSteps?: number;
105
+ deterministicGate?: Record<string, unknown>;
106
+ }): Promise<Record<string, unknown> | null>;
107
+ }
108
+ export declare function createAionisHttpLoopControlClient(options: AionisHttpClientOptions): AionisLoopControlClient;