@chorus-aidlc/chorus-openclaw-plugin 0.5.3 → 0.11.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/dist/connection-state.d.ts +35 -0
- package/dist/connection-state.d.ts.map +1 -0
- package/dist/connection-state.js +52 -0
- package/dist/connection-state.js.map +1 -0
- package/dist/control-handler.d.ts +73 -0
- package/dist/control-handler.d.ts.map +1 -0
- package/dist/control-handler.js +135 -0
- package/dist/control-handler.js.map +1 -0
- package/dist/daemon-client.d.ts +203 -0
- package/dist/daemon-client.d.ts.map +1 -0
- package/dist/daemon-client.js +469 -0
- package/dist/daemon-client.js.map +1 -0
- package/dist/daemon-rest-client.d.ts +86 -0
- package/dist/daemon-rest-client.d.ts.map +1 -0
- package/dist/daemon-rest-client.js +196 -0
- package/dist/daemon-rest-client.js.map +1 -0
- package/dist/event-router.d.ts +31 -6
- package/dist/event-router.d.ts.map +1 -1
- package/dist/event-router.js +58 -27
- package/dist/event-router.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -7
- package/dist/index.js.map +1 -1
- package/dist/lineage.d.ts +44 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +116 -0
- package/dist/lineage.js.map +1 -0
- package/dist/mcp-registration.d.ts.map +1 -1
- package/dist/mcp-registration.js +5 -4
- package/dist/mcp-registration.js.map +1 -1
- package/dist/sse-listener.d.ts +34 -0
- package/dist/sse-listener.d.ts.map +1 -1
- package/dist/sse-listener.js +78 -4
- package/dist/sse-listener.js.map +1 -1
- package/dist/wake.d.ts +20 -0
- package/dist/wake.d.ts.map +1 -1
- package/dist/wake.js +56 -0
- package/dist/wake.js.map +1 -1
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/chorus/SKILL.md +37 -6
- package/skills/develop/SKILL.md +1 -1
- package/skills/idea/SKILL.md +18 -3
- package/skills/openspec-aware/SKILL.md +1 -1
- package/skills/proposal/SKILL.md +1 -1
- package/skills/proposal-reviewer/SKILL.md +1 -1
- package/skills/quick-dev/SKILL.md +1 -1
- package/skills/review/SKILL.md +1 -1
- package/skills/task-reviewer/SKILL.md +1 -1
- package/skills/yolo/SKILL.md +1 -1
- package/src/connection-state.ts +66 -0
- package/src/control-handler.ts +219 -0
- package/src/daemon-client.ts +622 -0
- package/src/daemon-rest-client.ts +312 -0
- package/src/event-router.ts +103 -33
- package/src/index.ts +113 -8
- package/src/lineage.ts +157 -0
- package/src/mcp-registration.ts +6 -19
- package/src/openclaw-sdk.d.ts +232 -1
- package/src/sse-listener.ts +117 -5
- package/src/wake.ts +69 -26
package/src/index.ts
CHANGED
|
@@ -5,9 +5,15 @@ import { resolveConfig, validateConfigWithWarnings } from "./config.js";
|
|
|
5
5
|
import { ensureChorusMcpServer } from "./mcp-registration.js";
|
|
6
6
|
import { ChorusMcpClient } from "./mcp-client.js";
|
|
7
7
|
import { ChorusSseListener } from "./sse-listener.js";
|
|
8
|
-
import { ChorusEventRouter } from "./event-router.js";
|
|
9
|
-
import {
|
|
8
|
+
import { ChorusEventRouter, type WakeAttribution } from "./event-router.js";
|
|
9
|
+
import { resolveWakeRunContext } from "./wake.js";
|
|
10
10
|
import { registerChorusCommands } from "./commands.js";
|
|
11
|
+
import { ConnectionState } from "./connection-state.js";
|
|
12
|
+
import { createControlHandler, type ControlBehaviorHooks } from "./control-handler.js";
|
|
13
|
+
import { createDaemonRestClient } from "./daemon-rest-client.js";
|
|
14
|
+
import { LineageResolver } from "./lineage.js";
|
|
15
|
+
import { OpenClawDaemonClient, type WakeRequest } from "./daemon-client.js";
|
|
16
|
+
import type { DaemonPendingTurn } from "./daemon-rest-client.js";
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* JSON-Schema config contract for the Chorus plugin.
|
|
@@ -70,15 +76,97 @@ export default definePluginEntry({
|
|
|
70
76
|
// assignments, notifications back-fill).
|
|
71
77
|
const mcpClient = new ChorusMcpClient({ chorusUrl, apiKey, logger });
|
|
72
78
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
79
|
+
// 4b. Connection identity + reverse control channel + daemon reporting (parity).
|
|
80
|
+
// `connectionState` holds the DaemonConnection uuid the server reports
|
|
81
|
+
// post-handshake (captured by the listener's onConnectionId); it is the
|
|
82
|
+
// single source of truth for "which connection am I", read by the control
|
|
83
|
+
// handler's double-check AND the daemon REST reporter (lazily, so order
|
|
84
|
+
// doesn't matter — both predate the handshake).
|
|
85
|
+
const connectionState = new ConnectionState();
|
|
86
|
+
|
|
87
|
+
// The shared pure-REST daemon client owns the `/api/daemon/*` payload shapes
|
|
88
|
+
// (turn-advance / transcript / execution-state / report-interrupt /
|
|
89
|
+
// pending-turns). It reads the connectionUuid lazily from connectionState.
|
|
90
|
+
const restClient = createDaemonRestClient({
|
|
91
|
+
url: chorusUrl,
|
|
92
|
+
apiKey,
|
|
93
|
+
getConnectionUuid: () => connectionState.getConnectionUuid(),
|
|
94
|
+
logger,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Lineage resolver: per-notification { rootIdeaUuid, directIdeaUuid } via the
|
|
98
|
+
// root-idea REST endpoint, so the daemon client anchors the session on the
|
|
99
|
+
// DIRECT idea (resume/deliver_turn continuity) and reports the ROOT idea in
|
|
100
|
+
// its execution snapshot (the two-id contract).
|
|
101
|
+
const lineage = new LineageResolver({ url: chorusUrl, apiKey, logger });
|
|
102
|
+
|
|
103
|
+
// The in-process daemon client wraps runEmbeddedAgent with full reporting,
|
|
104
|
+
// the AbortController registry (real mid-run interrupt), the execution
|
|
105
|
+
// snapshot source, deterministic session-key mapping, and the at-most-once
|
|
106
|
+
// pending-turns backfill. `resolveRunContext` is the ONE place that reaches
|
|
107
|
+
// into api.config/api.runtime (kept out of the client so it stays testable).
|
|
108
|
+
// `redispatch` resolves lineage for a synthetic resume so it continues the
|
|
109
|
+
// SAME session, then runs the wake; a delivered turn already carries its ids.
|
|
110
|
+
let daemonClient: OpenClawDaemonClient;
|
|
111
|
+
const redispatch = (req: WakeRequest): void => {
|
|
112
|
+
void (async () => {
|
|
113
|
+
let enriched = req;
|
|
114
|
+
// A resume only knows the entity — resolve its lineage so the wake anchors on
|
|
115
|
+
// the same business key (direct idea) the original run used. A delivered turn
|
|
116
|
+
// already carries directIdeaUuid, so we skip the round-trip when present.
|
|
117
|
+
if (req.directIdeaUuid == null && req.entityType && req.entityUuid) {
|
|
118
|
+
try {
|
|
119
|
+
const { rootIdeaUuid, directIdeaUuid } = await lineage.resolve({
|
|
120
|
+
entityType: req.entityType,
|
|
121
|
+
entityUuid: req.entityUuid,
|
|
122
|
+
});
|
|
123
|
+
enriched = { ...req, rootIdeaUuid, directIdeaUuid };
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.warn(`[Chorus] resume lineage resolve failed: ${err}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
await daemonClient.runWake(enriched);
|
|
129
|
+
})();
|
|
130
|
+
};
|
|
131
|
+
daemonClient = new OpenClawDaemonClient({
|
|
132
|
+
restClient,
|
|
133
|
+
resolveRunContext: () => resolveWakeRunContext(api, logger),
|
|
134
|
+
redispatch,
|
|
135
|
+
// Build the prompt for a delivered human_instruction turn. The free-text body
|
|
136
|
+
// lives only on the turn (promptText); fall back to a generic nudge if absent.
|
|
137
|
+
buildTurnPrompt: (turn: DaemonPendingTurn) =>
|
|
138
|
+
turn.promptText && turn.promptText.trim()
|
|
139
|
+
? `[Chorus] A human sent you an instruction in this conversation:\n\n${turn.promptText}`
|
|
140
|
+
: `[Chorus] A human sent you a new instruction in this conversation (session ${turn.sessionId}). Review the latest comments and respond.`,
|
|
141
|
+
logger,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// The control handler ROUTES verified control commands to the daemon client's
|
|
145
|
+
// behavior hooks (real abort / resume re-dispatch / pending-turns sweep),
|
|
146
|
+
// after its own double-check (own connection + held entity).
|
|
147
|
+
const controlHooks: ControlBehaviorHooks = daemonClient.controlHooks;
|
|
148
|
+
const onControl = createControlHandler({ connectionState, hooks: controlHooks, logger });
|
|
149
|
+
|
|
150
|
+
// 5. Event router. Wakes the agent in-process by running an embedded agent turn
|
|
151
|
+
// via the daemon client (which calls api.runtime.agent.runEmbeddedAgent and
|
|
152
|
+
// reports lifecycle/transcript). The router resolves each notification's
|
|
153
|
+
// lineage, then the daemon client's runWake gracefully DROPS (logs + returns)
|
|
154
|
+
// when it cannot run — it never throws, so the SSE service stays alive.
|
|
155
|
+
const wakeFn = (message: string, contextKey: string, attribution?: WakeAttribution): void => {
|
|
156
|
+
void daemonClient.runWake({
|
|
157
|
+
prompt: message,
|
|
158
|
+
contextKey,
|
|
159
|
+
entityType: attribution?.entityType,
|
|
160
|
+
entityUuid: attribution?.entityUuid,
|
|
161
|
+
directIdeaUuid: attribution?.directIdeaUuid,
|
|
162
|
+
rootIdeaUuid: attribution?.rootIdeaUuid,
|
|
163
|
+
});
|
|
164
|
+
};
|
|
78
165
|
const eventRouter = new ChorusEventRouter({
|
|
79
166
|
mcpClient,
|
|
80
167
|
logger,
|
|
81
|
-
|
|
168
|
+
lineage,
|
|
169
|
+
wake: wakeFn,
|
|
82
170
|
});
|
|
83
171
|
|
|
84
172
|
// 6. Background SSE service. The SSE socket opens only inside start(), which
|
|
@@ -92,7 +180,23 @@ export default definePluginEntry({
|
|
|
92
180
|
apiKey,
|
|
93
181
|
logger,
|
|
94
182
|
onEvent: (event) => eventRouter.dispatch(event),
|
|
183
|
+
// Capture (and refresh on reconnect) the DaemonConnection identity the
|
|
184
|
+
// server reports post-handshake. NOT a wake — forked by the listener.
|
|
185
|
+
onConnectionId: (connectionUuid) => {
|
|
186
|
+
connectionState.setConnectionUuid(connectionUuid);
|
|
187
|
+
logger.info(`[Chorus] registered as daemon connection ${connectionUuid}`);
|
|
188
|
+
},
|
|
189
|
+
// Reverse control channel. The handler does the double-check and routes
|
|
190
|
+
// to the behavior hooks — NEVER the wake path.
|
|
191
|
+
onControl,
|
|
95
192
|
onReconnect: async () => {
|
|
193
|
+
// (1) Notification backfill — re-pull unread notifications missed during
|
|
194
|
+
// the gap (autonomous wakes). (2) Pending-turns backfill — re-derive
|
|
195
|
+
// this connection's unstarted human_instruction turns from the turn
|
|
196
|
+
// table and run each (the lost-deliver_turn-ping safety net). The two
|
|
197
|
+
// share the daemon client's seen-set so a turn is run at most once
|
|
198
|
+
// across live delivery + backfill. Each swallows its own errors so one
|
|
199
|
+
// failing source never aborts the other.
|
|
96
200
|
try {
|
|
97
201
|
const result = (await mcpClient.callTool("chorus_get_notifications", {
|
|
98
202
|
status: "unread",
|
|
@@ -105,6 +209,7 @@ export default definePluginEntry({
|
|
|
105
209
|
} catch (err) {
|
|
106
210
|
logger.warn(`Failed to back-fill notifications: ${err}`);
|
|
107
211
|
}
|
|
212
|
+
await daemonClient.onReconnect();
|
|
108
213
|
},
|
|
109
214
|
});
|
|
110
215
|
await sseListener.connect();
|
package/src/lineage.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// packages/openclaw-plugin/src/lineage.ts
|
|
2
|
+
// Resolves any inbound Chorus notification/entity to its idea attribution, so the
|
|
3
|
+
// OpenClaw daemon client can anchor ONE embedded-agent session per DIRECT idea (the
|
|
4
|
+
// in-process analog of `claude --resume <directIdeaUuid>`) while reporting the ROOT
|
|
5
|
+
// idea in its execution snapshot for observability.
|
|
6
|
+
//
|
|
7
|
+
// TS mirror of `cli/lineage.mjs` (`LineageResolver`) — same single-source-of-truth
|
|
8
|
+
// REST contract, re-stated in TS because the plugin publishes standalone (it cannot
|
|
9
|
+
// import a file under `cli/`; see daemon-rest-client.ts for the same rationale).
|
|
10
|
+
//
|
|
11
|
+
// Resolution is fully SERVER-SIDE: every entity is resolved by a single call to the
|
|
12
|
+
// standalone REST endpoint
|
|
13
|
+
// GET /api/entities/{type}/{uuid}/root-idea (Bearer <cho_ agent key>)
|
|
14
|
+
// which returns BOTH `rootIdeaUuid` (topmost ancestor) and `directIdeaUuid` (the
|
|
15
|
+
// first idea node on the lineage). There is intentionally NO client-side lineage
|
|
16
|
+
// walk. On any failure (unreachable server, non-2xx, malformed body) it returns both
|
|
17
|
+
// ids as null so the caller falls back to a per-entity session key — "no idea
|
|
18
|
+
// ancestor" is a normal, non-fatal outcome. Uses global fetch (Node 18+) → no new dep.
|
|
19
|
+
|
|
20
|
+
export interface LineageLogger {
|
|
21
|
+
info: (msg: string) => void;
|
|
22
|
+
warn: (msg: string) => void;
|
|
23
|
+
error: (msg: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const NOOP_LOGGER: LineageLogger = { info() {}, warn() {}, error() {} };
|
|
27
|
+
|
|
28
|
+
/** The idea attribution of an entity. Both null when there's no idea ancestor. */
|
|
29
|
+
export interface LineageAttribution {
|
|
30
|
+
rootIdeaUuid: string | null;
|
|
31
|
+
directIdeaUuid: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const NONE: LineageAttribution = { rootIdeaUuid: null, directIdeaUuid: null };
|
|
35
|
+
|
|
36
|
+
export interface LineageResolverOptions {
|
|
37
|
+
/** Chorus base URL. */
|
|
38
|
+
url: string;
|
|
39
|
+
/** `cho_` agent API key. */
|
|
40
|
+
apiKey: string;
|
|
41
|
+
logger?: LineageLogger;
|
|
42
|
+
/** Injectable for tests (defaults to global fetch). */
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class LineageResolver {
|
|
47
|
+
private readonly url: string;
|
|
48
|
+
private readonly apiKey: string;
|
|
49
|
+
private readonly logger: LineageLogger;
|
|
50
|
+
private readonly fetchImpl: typeof fetch;
|
|
51
|
+
/** Per-run cache keyed by `${type}:${uuid}` so repeats single-flight. */
|
|
52
|
+
private readonly cache = new Map<string, LineageAttribution>();
|
|
53
|
+
|
|
54
|
+
constructor(opts: LineageResolverOptions) {
|
|
55
|
+
this.url = opts.url.replace(/\/$/, "");
|
|
56
|
+
this.apiKey = opts.apiKey;
|
|
57
|
+
this.logger = opts.logger ?? NOOP_LOGGER;
|
|
58
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an entity to its idea attribution `{ rootIdeaUuid, directIdeaUuid }`.
|
|
63
|
+
* One REST call per entity; the cache single-flights repeats. On any failure both
|
|
64
|
+
* ids are null (caller falls back to a per-entity key). Never throws.
|
|
65
|
+
*/
|
|
66
|
+
async resolve(event: {
|
|
67
|
+
entityType?: string;
|
|
68
|
+
entityUuid?: string;
|
|
69
|
+
}): Promise<LineageAttribution> {
|
|
70
|
+
const entityType = event?.entityType;
|
|
71
|
+
const entityUuid = event?.entityUuid;
|
|
72
|
+
if (!entityType || !entityUuid) {
|
|
73
|
+
this.logger.warn("[Chorus] lineage: event missing entityType/entityUuid");
|
|
74
|
+
return NONE;
|
|
75
|
+
}
|
|
76
|
+
// An ad-hoc conversation (`daemon_session`) has NO idea ancestor by definition,
|
|
77
|
+
// and the root-idea endpoint does not accept it (it would 400). Short-circuit to
|
|
78
|
+
// the null attribution the caller would fall back to anyway — avoiding a
|
|
79
|
+
// guaranteed-failing round-trip + a spurious warn on every ad-hoc resume. The
|
|
80
|
+
// caller then anchors the session on the entity uuid (= the ad-hoc sessionId).
|
|
81
|
+
if (entityType === "daemon_session") {
|
|
82
|
+
return NONE;
|
|
83
|
+
}
|
|
84
|
+
const cacheKey = `${entityType}:${entityUuid}`;
|
|
85
|
+
const cached = this.cache.get(cacheKey);
|
|
86
|
+
if (cached) return cached;
|
|
87
|
+
|
|
88
|
+
const result = await this.resolveViaServer(entityType, entityUuid);
|
|
89
|
+
this.cache.set(cacheKey, result);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Call GET /api/entities/{type}/{uuid}/root-idea and return
|
|
95
|
+
* `{ rootIdeaUuid, directIdeaUuid }` (each string | null). Returns both null on any
|
|
96
|
+
* error so the caller degrades to a per-entity session key — never throws.
|
|
97
|
+
*/
|
|
98
|
+
private async resolveViaServer(
|
|
99
|
+
entityType: string,
|
|
100
|
+
entityUuid: string,
|
|
101
|
+
): Promise<LineageAttribution> {
|
|
102
|
+
const endpoint =
|
|
103
|
+
`${this.url}/api/entities/${encodeURIComponent(entityType)}/` +
|
|
104
|
+
`${encodeURIComponent(entityUuid)}/root-idea`;
|
|
105
|
+
let response: Response;
|
|
106
|
+
try {
|
|
107
|
+
response = await this.fetchImpl(endpoint, {
|
|
108
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, Accept: "application/json" },
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
this.logger.warn(`[Chorus] lineage: request failed for ${entityType}:${entityUuid}: ${err}`);
|
|
112
|
+
return NONE;
|
|
113
|
+
}
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
this.logger.warn(
|
|
116
|
+
`[Chorus] lineage: server returned ${response.status} for ${entityType}:${entityUuid}`,
|
|
117
|
+
);
|
|
118
|
+
return NONE;
|
|
119
|
+
}
|
|
120
|
+
let body: unknown;
|
|
121
|
+
try {
|
|
122
|
+
body = await response.json();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
this.logger.warn(`[Chorus] lineage: bad JSON for ${entityType}:${entityUuid}: ${err}`);
|
|
125
|
+
return NONE;
|
|
126
|
+
}
|
|
127
|
+
// API envelope: { success: true, data: { rootIdeaUuid, directIdeaUuid, ... } }.
|
|
128
|
+
const data =
|
|
129
|
+
body && typeof body === "object" ? (body as { data?: unknown }).data : undefined;
|
|
130
|
+
if (!data || typeof data !== "object" || !("rootIdeaUuid" in data)) {
|
|
131
|
+
this.logger.warn(
|
|
132
|
+
`[Chorus] lineage: unexpected response shape for ${entityType}:${entityUuid}`,
|
|
133
|
+
);
|
|
134
|
+
return NONE;
|
|
135
|
+
}
|
|
136
|
+
const root = (data as { rootIdeaUuid: unknown }).rootIdeaUuid;
|
|
137
|
+
if (root !== null && typeof root !== "string") {
|
|
138
|
+
this.logger.warn(`[Chorus] lineage: non-string rootIdeaUuid for ${entityType}:${entityUuid}`);
|
|
139
|
+
return NONE;
|
|
140
|
+
}
|
|
141
|
+
// directIdeaUuid is the session anchor. Older servers may omit it: treat a
|
|
142
|
+
// missing/non-string value as null so the caller falls back to a per-entity key.
|
|
143
|
+
const directRaw = (data as { directIdeaUuid?: unknown }).directIdeaUuid;
|
|
144
|
+
const direct = typeof directRaw === "string" ? directRaw : null;
|
|
145
|
+
if (directRaw !== undefined && directRaw !== null && typeof directRaw !== "string") {
|
|
146
|
+
this.logger.warn(
|
|
147
|
+
`[Chorus] lineage: non-string directIdeaUuid for ${entityType}:${entityUuid}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const resolvedVia = (data as { resolvedVia?: unknown }).resolvedVia;
|
|
151
|
+
this.logger.info(
|
|
152
|
+
`[Chorus] lineage: ${entityType}:${entityUuid} → root ${root ?? "none"}, direct ${direct ?? "none"}` +
|
|
153
|
+
(typeof resolvedVia === "string" ? ` (${resolvedVia})` : ""),
|
|
154
|
+
);
|
|
155
|
+
return { rootIdeaUuid: root, directIdeaUuid: direct };
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/mcp-registration.ts
CHANGED
|
@@ -86,25 +86,12 @@ export async function ensureChorusMcpServer(
|
|
|
86
86
|
const desired = buildDesiredEntry(cfg.chorusUrl!, cfg.apiKey!);
|
|
87
87
|
|
|
88
88
|
try {
|
|
89
|
-
// `api.runtime` is
|
|
90
|
-
// config
|
|
91
|
-
//
|
|
92
|
-
// (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
| {
|
|
96
|
-
config?: {
|
|
97
|
-
current?: () => { mcp?: { servers?: Record<string, unknown> } } | undefined;
|
|
98
|
-
mutateConfigFile?: (params: {
|
|
99
|
-
afterWrite: { mode: "auto" };
|
|
100
|
-
mutate: (draft: {
|
|
101
|
-
mcp?: { servers?: Record<string, unknown> };
|
|
102
|
-
}) => void;
|
|
103
|
-
}) => Promise<unknown>;
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
| undefined
|
|
107
|
-
)?.config;
|
|
89
|
+
// `api.runtime` is now typed (`OpenClawPluginRuntime`) via the SDK shim; its
|
|
90
|
+
// `config` slice mirrors the real `PluginRuntimeCore.config` API
|
|
91
|
+
// (../openclaw/src/plugins/runtime/types-core.ts:145) — `current()` reads the
|
|
92
|
+
// live snapshot, `mutateConfigFile(...)` writes the `mcp.servers.chorus`
|
|
93
|
+
// entry. No `unknown` cast needed.
|
|
94
|
+
const runtimeConfig = api.runtime?.config;
|
|
108
95
|
|
|
109
96
|
if (!runtimeConfig?.mutateConfigFile) {
|
|
110
97
|
logger.error(
|
package/src/openclaw-sdk.d.ts
CHANGED
|
@@ -42,11 +42,236 @@ declare module "openclaw/plugin-sdk/plugin-entry" {
|
|
|
42
42
|
jsonSchema?: Record<string, unknown>;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
// ===========================================================================
|
|
46
|
+
// Runtime SDK surface (`api.runtime`) — typed against the REAL ../openclaw
|
|
47
|
+
// source so the daemon call sites are compile-time checked rather than
|
|
48
|
+
// `unknown`-cast. We declare ONLY the minimal slice the plugin consumes; the
|
|
49
|
+
// full `PluginRuntime` graph (subagent/nodes/channel/media/...) is not pulled
|
|
50
|
+
// in. Every shape below was verified field-by-field against the real source
|
|
51
|
+
// (file:line citations inline) — NOT from memory.
|
|
52
|
+
//
|
|
53
|
+
// WHY HAND-DECLARED (not imported from `openclaw/plugin-sdk`): the real
|
|
54
|
+
// package exports `./plugin-sdk` (package.json exports map) but NOT the
|
|
55
|
+
// `./plugin-sdk/plugin-entry` subpath this plugin imports, and the `openclaw`
|
|
56
|
+
// build resolvable in this workspace is an older 2026.3.x that lacks both.
|
|
57
|
+
// Importing the real defs would also couple this separately-published package
|
|
58
|
+
// to the full `openclaw` type graph (llm-core, markdown-core, …) and turn the
|
|
59
|
+
// peer into a build-time hard dependency. The plugin must build/pack
|
|
60
|
+
// standalone (peerDependencies.openclaw only), so we mirror the minimal slice.
|
|
61
|
+
// ===========================================================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `BlockReplyPayload` — payload for the `onBlockReply` streaming callback.
|
|
65
|
+
* Verified: ../openclaw/src/agents/embedded-agent-payloads.ts:1-11.
|
|
66
|
+
* The plugin's transcript reporter reads only `.text`.
|
|
67
|
+
*/
|
|
68
|
+
export type OpenClawBlockReplyPayload = {
|
|
69
|
+
text?: string;
|
|
70
|
+
mediaUrls?: string[];
|
|
71
|
+
audioAsVoice?: boolean;
|
|
72
|
+
trustedLocalMedia?: boolean;
|
|
73
|
+
sensitiveMedia?: boolean;
|
|
74
|
+
isReasoning?: boolean;
|
|
75
|
+
replyToId?: string;
|
|
76
|
+
replyToTag?: boolean;
|
|
77
|
+
replyToCurrent?: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `ReplyPayload` (subset) — payload for the `onToolResult` streaming callback.
|
|
82
|
+
* Verified: ../openclaw/src/auto-reply/reply-payload.ts:7-60 (only the fields
|
|
83
|
+
* the plugin may read are declared; the rest of the large union is omitted via
|
|
84
|
+
* the index signature).
|
|
85
|
+
*/
|
|
86
|
+
export type OpenClawReplyPayload = {
|
|
87
|
+
text?: string;
|
|
88
|
+
mediaUrl?: string;
|
|
89
|
+
mediaUrls?: string[];
|
|
90
|
+
isError?: boolean;
|
|
91
|
+
isReasoning?: boolean;
|
|
92
|
+
isReasoningSnapshot?: boolean;
|
|
93
|
+
channelData?: Record<string, unknown>;
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* What initiated an embedded run.
|
|
99
|
+
* Verified: ../openclaw/src/agents/embedded-agent-runner/run/params.ts:32
|
|
100
|
+
* (`EmbeddedRunTrigger`).
|
|
101
|
+
*/
|
|
102
|
+
export type OpenClawEmbeddedRunTrigger =
|
|
103
|
+
| "cron"
|
|
104
|
+
| "heartbeat"
|
|
105
|
+
| "manual"
|
|
106
|
+
| "memory"
|
|
107
|
+
| "overflow"
|
|
108
|
+
| "user";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Params for `runtime.agent.runEmbeddedAgent`.
|
|
112
|
+
*
|
|
113
|
+
* The real `RunEmbeddedAgentParams` has ~100 fields, almost all optional;
|
|
114
|
+
* verified against ../openclaw/src/agents/embedded-agent-runner/run/params.ts.
|
|
115
|
+
* We declare exactly the fields the plugin passes/uses (required ones with
|
|
116
|
+
* their real required/optional-ness) plus the daemon-parity fields
|
|
117
|
+
* (`abortSignal` + per-message streaming callbacks) the dependent tasks
|
|
118
|
+
* consume, and keep an index signature for the untouched remainder so the
|
|
119
|
+
* type stays a faithful subset rather than a closed shape.
|
|
120
|
+
*
|
|
121
|
+
* Required-field cites (params.ts): sessionId:40, sessionFile:104,
|
|
122
|
+
* workspaceDir:105, prompt:111, timeoutMs:155, runId:166.
|
|
123
|
+
* Optional-field cites: sessionKey:41, agentId:46, trigger:51, agentDir:108,
|
|
124
|
+
* config:109, provider:122, model:123, disableMessageTool:91,
|
|
125
|
+
* runTimeoutOverrideMs:165, abortSignal:167, onAssistantMessageStart:190,
|
|
126
|
+
* onBlockReply:191, onReasoningStream:195-199, onToolResult:201.
|
|
127
|
+
*/
|
|
128
|
+
export type RunEmbeddedAgentParams = {
|
|
129
|
+
sessionId: string;
|
|
130
|
+
sessionFile: string;
|
|
131
|
+
workspaceDir: string;
|
|
132
|
+
prompt: string;
|
|
133
|
+
timeoutMs: number;
|
|
134
|
+
runId: string;
|
|
135
|
+
sessionKey?: string;
|
|
136
|
+
agentId?: string;
|
|
137
|
+
trigger?: OpenClawEmbeddedRunTrigger;
|
|
138
|
+
agentDir?: string;
|
|
139
|
+
// The real type is `OpenClawConfig`; the plugin passes through the opaque
|
|
140
|
+
// `api.config` snapshot, so `unknown` keeps it pass-through-safe.
|
|
141
|
+
config?: unknown;
|
|
142
|
+
provider?: string;
|
|
143
|
+
model?: string;
|
|
144
|
+
disableMessageTool?: boolean;
|
|
145
|
+
runTimeoutOverrideMs?: number;
|
|
146
|
+
/** Cooperative mid-run interrupt; relayed through the whole run. (params.ts:167) */
|
|
147
|
+
abortSignal?: AbortSignal;
|
|
148
|
+
/** Fires when the assistant begins a message. (params.ts:190) */
|
|
149
|
+
onAssistantMessageStart?: () => void | Promise<void>;
|
|
150
|
+
/** Fires per finalized assistant text block — the transcript source. (params.ts:191) */
|
|
151
|
+
onBlockReply?: (payload: OpenClawBlockReplyPayload) => void | Promise<void>;
|
|
152
|
+
/** Fires for reasoning/thinking deltas; NOT posted to the transcript. (params.ts:195) */
|
|
153
|
+
onReasoningStream?: (payload: {
|
|
154
|
+
text?: string;
|
|
155
|
+
mediaUrls?: string[];
|
|
156
|
+
isReasoningSnapshot?: boolean;
|
|
157
|
+
}) => void | Promise<void>;
|
|
158
|
+
/** Fires per tool result. (params.ts:201) */
|
|
159
|
+
onToolResult?: (payload: OpenClawReplyPayload) => void | Promise<void>;
|
|
160
|
+
// The real type carries many more optional fields; allow them without
|
|
161
|
+
// re-declaring the full graph.
|
|
162
|
+
[key: string]: unknown;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Result of `runtime.agent.runEmbeddedAgent`.
|
|
167
|
+
* Verified: ../openclaw/src/agents/embedded-agent-runner/types.ts:179-212
|
|
168
|
+
* (`EmbeddedAgentRunResult`) and its `meta: EmbeddedAgentRunMeta` at :137-177.
|
|
169
|
+
* The plugin reads `meta.aborted` to distinguish a user-abort from a crash.
|
|
170
|
+
*/
|
|
171
|
+
export type EmbeddedAgentRunMeta = {
|
|
172
|
+
durationMs: number;
|
|
173
|
+
/** True when the run was aborted via `abortSignal`. (types.ts:140) */
|
|
174
|
+
aborted?: boolean;
|
|
175
|
+
finalAssistantVisibleText?: string;
|
|
176
|
+
stopReason?: string;
|
|
177
|
+
[key: string]: unknown;
|
|
178
|
+
};
|
|
179
|
+
export type EmbeddedAgentRunResult = {
|
|
180
|
+
meta: EmbeddedAgentRunMeta;
|
|
181
|
+
payloads?: Array<{ text?: string; isError?: boolean; isReasoning?: boolean }>;
|
|
182
|
+
[key: string]: unknown;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Session store entry (subset).
|
|
187
|
+
* Verified: ../openclaw/src/config/sessions/types.ts — `sessionId:254`,
|
|
188
|
+
* `sessionFile?:256`. Returned by `getSessionEntry`.
|
|
189
|
+
*/
|
|
190
|
+
export type OpenClawSessionEntry = {
|
|
191
|
+
sessionId: string;
|
|
192
|
+
sessionFile?: string;
|
|
193
|
+
[key: string]: unknown;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The `runtime.agent` slice the plugin consumes.
|
|
198
|
+
* Verified: ../openclaw/src/plugins/runtime/types-core.ts:180-221
|
|
199
|
+
* (`PluginRuntimeCore.agent`). Each member's signature confirmed at its real
|
|
200
|
+
* definition site (cited per member).
|
|
201
|
+
*/
|
|
202
|
+
export type OpenClawRuntimeAgent = {
|
|
203
|
+
/** params.ts/run.ts:458 — `(params) => Promise<EmbeddedAgentRunResult>`. */
|
|
204
|
+
runEmbeddedAgent: (params: RunEmbeddedAgentParams) => Promise<EmbeddedAgentRunResult>;
|
|
205
|
+
/** agent-scope-config.ts:195 — positional `(cfg, agentId)`, returns the agent dir. */
|
|
206
|
+
resolveAgentDir: (cfg: unknown, agentId: string) => string;
|
|
207
|
+
/** agent-scope-config.ts:170 — positional `(cfg, agentId)`, returns the workspace dir. */
|
|
208
|
+
resolveAgentWorkspaceDir: (cfg: unknown, agentId: string) => string;
|
|
209
|
+
/** timeout.ts:15 — options-object `({ cfg }) => number`. */
|
|
210
|
+
resolveAgentTimeoutMs: (opts: {
|
|
211
|
+
cfg?: unknown;
|
|
212
|
+
overrideMs?: number | null;
|
|
213
|
+
overrideSeconds?: number | null;
|
|
214
|
+
minMs?: number;
|
|
215
|
+
}) => number;
|
|
216
|
+
session: {
|
|
217
|
+
/** store.ts:210 — `({ sessionKey, agentId? }) => SessionEntry | undefined`. */
|
|
218
|
+
getSessionEntry: (options: {
|
|
219
|
+
sessionKey: string;
|
|
220
|
+
agentId?: string;
|
|
221
|
+
env?: NodeJS.ProcessEnv;
|
|
222
|
+
storePath?: string;
|
|
223
|
+
}) => OpenClawSessionEntry | undefined;
|
|
224
|
+
/** paths.ts:267 — `(sessionId, entry?, opts?) => string`. */
|
|
225
|
+
resolveSessionFilePath: (
|
|
226
|
+
sessionId: string,
|
|
227
|
+
entry?: { sessionFile?: string },
|
|
228
|
+
opts?: { agentId?: string; sessionsDir?: string },
|
|
229
|
+
) => string;
|
|
230
|
+
[key: string]: unknown;
|
|
231
|
+
};
|
|
232
|
+
[key: string]: unknown;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The `runtime.config` slice the plugin consumes (MCP-server registration).
|
|
237
|
+
* Verified: ../openclaw/src/plugins/runtime/types-core.ts:145-178
|
|
238
|
+
* (`PluginRuntimeCore.config`). The plugin uses `current()` to read the live
|
|
239
|
+
* config snapshot and `mutateConfigFile(...)` to write the `mcp.servers.chorus`
|
|
240
|
+
* entry; the real `mutateConfigFile` is generic — `mutate(draft)` mutates a
|
|
241
|
+
* `DeepReadonly`-cloned draft in place. We narrow `draft`/return to the MCP
|
|
242
|
+
* slice the plugin touches.
|
|
243
|
+
*/
|
|
244
|
+
export type OpenClawRuntimeConfig = {
|
|
245
|
+
current?: () => { mcp?: { servers?: Record<string, unknown> } } | undefined;
|
|
246
|
+
mutateConfigFile?: (params: {
|
|
247
|
+
afterWrite: { mode: "auto" };
|
|
248
|
+
mutate: (draft: { mcp?: { servers?: Record<string, unknown> } }) => void;
|
|
249
|
+
}) => Promise<unknown>;
|
|
250
|
+
[key: string]: unknown;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The slice of `PluginRuntime` (`api.runtime`) the plugin consumes.
|
|
255
|
+
* Verified: ../openclaw/src/plugins/types.ts:2600 (`runtime: PluginRuntime`)
|
|
256
|
+
* → ../openclaw/src/plugins/runtime/types.ts (PluginRuntime = PluginRuntimeCore
|
|
257
|
+
* & …) → ../openclaw/src/plugins/runtime/types-core.ts (`agent` :180,
|
|
258
|
+
* `config` :145). The reverse control channel is NOT part of this runtime
|
|
259
|
+
* surface — it arrives over the SSE stream (`type: "control"`), handled in
|
|
260
|
+
* `sse-listener.ts` / the control handler, not via `api.runtime`. Confirmed:
|
|
261
|
+
* no `control`/`connection` member exists on `PluginRuntimeCore`.
|
|
262
|
+
*/
|
|
263
|
+
export type OpenClawPluginRuntime = {
|
|
264
|
+
agent: OpenClawRuntimeAgent;
|
|
265
|
+
config: OpenClawRuntimeConfig;
|
|
266
|
+
[key: string]: unknown;
|
|
267
|
+
};
|
|
268
|
+
|
|
45
269
|
/**
|
|
46
270
|
* Permissive subset of `OpenClawPluginApi` used by this plugin's entry.
|
|
47
271
|
*
|
|
48
272
|
* Only the members this plugin touches are typed; the index signature keeps
|
|
49
273
|
* the rest of the (large) host API accessible without importing it.
|
|
274
|
+
* Verified: ../openclaw/src/plugins/types.ts:2584-2600 (`OpenClawPluginApi`).
|
|
50
275
|
*/
|
|
51
276
|
export type OpenClawPluginApi = {
|
|
52
277
|
registrationMode: PluginRegistrationMode;
|
|
@@ -68,7 +293,13 @@ declare module "openclaw/plugin-sdk/plugin-entry" {
|
|
|
68
293
|
}) => void;
|
|
69
294
|
registerCommand: (command: unknown) => void;
|
|
70
295
|
registerTool: (tool: unknown, opts?: unknown) => void;
|
|
71
|
-
|
|
296
|
+
/**
|
|
297
|
+
* In-process runtime helpers. Typed to the minimal slice the plugin uses
|
|
298
|
+
* (`agent`, `config`) — no longer bare `unknown`. Optional because the
|
|
299
|
+
* narrow registration modes (discovery/cli-metadata) may not expose it; the
|
|
300
|
+
* plugin guards `api.runtime` before use.
|
|
301
|
+
*/
|
|
302
|
+
runtime?: OpenClawPluginRuntime;
|
|
72
303
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
304
|
[key: string]: any;
|
|
74
305
|
};
|