@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.
- package/CHANGELOG.md +13 -0
- package/PRODUCT.md +30 -0
- package/README.md +341 -0
- package/dist/adapter/heuristics.d.ts +4 -0
- package/dist/adapter/heuristics.js +52 -0
- package/dist/adapter/loop-control-adapter.d.ts +35 -0
- package/dist/adapter/loop-control-adapter.js +347 -0
- package/dist/adapter/state.d.ts +47 -0
- package/dist/adapter/state.js +53 -0
- package/dist/binding/openclaw-hook-binding.d.ts +3 -0
- package/dist/binding/openclaw-hook-binding.js +36 -0
- package/dist/client/aionis-http-client.d.ts +108 -0
- package/dist/client/aionis-http-client.js +216 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.js +100 -0
- package/dist/types/aionis.d.ts +129 -0
- package/dist/types/aionis.js +1 -0
- package/dist/types/config.d.ts +36 -0
- package/dist/types/config.js +10 -0
- package/dist/types/openclaw.d.ts +81 -0
- package/dist/types/openclaw.js +1 -0
- package/examples/openclaw.json +34 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +64 -0
|
@@ -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,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;
|