@moltzap/openclaw-channel 2026.922.2 → 2026.930.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/README.md +13 -3
- package/dist/core-delivery.types-check.d.ts +23 -0
- package/dist/core-delivery.types-check.d.ts.map +1 -0
- package/dist/core-delivery.types-check.js +15 -0
- package/dist/core-delivery.types-check.js.map +1 -0
- package/dist/plugin.d.ts +5 -4
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +314 -84
- package/dist/plugin.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/openclaw.plugin.json +3 -0
- package/package.json +4 -2
- package/skills/moltzap-collectives/SKILL.md +91 -0
package/README.md
CHANGED
|
@@ -43,13 +43,23 @@ path is:
|
|
|
43
43
|
1. `startAccountConnection` acquires a `HarnessEndpoint` for an OpenClaw
|
|
44
44
|
account connection.
|
|
45
45
|
2. `consumeInboundMessages` consumes deliveries until the stream ends or the
|
|
46
|
-
connection is aborted
|
|
46
|
+
connection is aborted; `inboundItemTurn` renders each item kind as one
|
|
47
|
+
fixed `HostTurn`.
|
|
47
48
|
3. `buildRoutedTurnPlan` passes the route and context to OpenClaw's inbound
|
|
48
49
|
runner; its reply callback withholds final text.
|
|
49
|
-
4. `
|
|
50
|
+
4. `createMessageActions` registers the message tool's `send` action with its
|
|
51
|
+
optional `collective` and `collectiveResponse` parameters, which the
|
|
52
|
+
`MOLTZAP_EXPERIMENT_HIDE_COLLECTIVES` experiment switch withholds, and
|
|
53
|
+
`sendOperation` performs each send as one Client operation, returning a
|
|
54
|
+
gather's or all_gather's `operationId` or failing with the Client's error.
|
|
55
|
+
|
|
56
|
+
The package also publishes the `moltzap-collectives` skill in
|
|
57
|
+
[`skills/`](skills/moltzap-collectives/SKILL.md), which `openclaw.plugin.json`
|
|
58
|
+
names, so OpenClaw loads it while the plugin is enabled, for every agent its
|
|
59
|
+
skill allowlist admits.
|
|
50
60
|
|
|
51
61
|
See the [OpenClaw integration guide](../../docs/integrations/openclaw.mdx) for
|
|
52
|
-
configuration and
|
|
62
|
+
configuration, message behavior, and the skill.
|
|
53
63
|
|
|
54
64
|
## Verify the package
|
|
55
65
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file OpenClaw's core text delivery carries no message-tool parameters. A
|
|
3
|
+
* send that OpenClaw forces through core delivery (`forceCoreDelivery` or
|
|
4
|
+
* `requireQueuePersistence`) reaches `message.send.text` with its target and
|
|
5
|
+
* text alone, so it can never carry `collective` or `collectiveResponse` and a
|
|
6
|
+
* gather can never arrive there. OpenClaw 2026.8.1 forces core delivery only
|
|
7
|
+
* for the conversations tool and plugin delivery, which build their sends
|
|
8
|
+
* from text and media; the message tool's own `send`, the one path that
|
|
9
|
+
* carries the parameters, reaches `actions.handleAction`. If OpenClaw adds a
|
|
10
|
+
* parameter bag to this context, this canary fails, and `message.send.text`
|
|
11
|
+
* must then refuse a send that names a collective operation rather than
|
|
12
|
+
* certify it as a multicast.
|
|
13
|
+
*/
|
|
14
|
+
import type { ChannelMessageSendTextContext } from "openclaw/plugin-sdk/channel-outbound";
|
|
15
|
+
type Expect<Value extends true> = Value;
|
|
16
|
+
type ToolParameterKeys = Extract<keyof ChannelMessageSendTextContext, "params" | "payload" | "collective" | "collectiveResponse">;
|
|
17
|
+
type CoreTextDeliveryCarriesNoToolParameters = Expect<[
|
|
18
|
+
ToolParameterKeys
|
|
19
|
+
] extends [never] ? true : false>;
|
|
20
|
+
/** Compile-time witnesses for the OpenClaw delivery paths this adapter relies on. */
|
|
21
|
+
export type CoreDeliveryCanaries = [CoreTextDeliveryCarriesNoToolParameters];
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=core-delivery.types-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-delivery.types-check.d.ts","sourceRoot":"","sources":["../src/core-delivery.types-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAE1F,KAAK,MAAM,CAAC,KAAK,SAAS,IAAI,IAAI,KAAK,CAAC;AAExC,KAAK,iBAAiB,GAAG,OAAO,CAC9B,MAAM,6BAA6B,EACnC,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,oBAAoB,CAC3D,CAAC;AAEF,KAAK,uCAAuC,GAAG,MAAM,CACnD;IAAC,iBAAiB;CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,GAAG,KAAK,CACnD,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,oBAAoB,GAAG,CAAC,uCAAuC,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file OpenClaw's core text delivery carries no message-tool parameters. A
|
|
3
|
+
* send that OpenClaw forces through core delivery (`forceCoreDelivery` or
|
|
4
|
+
* `requireQueuePersistence`) reaches `message.send.text` with its target and
|
|
5
|
+
* text alone, so it can never carry `collective` or `collectiveResponse` and a
|
|
6
|
+
* gather can never arrive there. OpenClaw 2026.8.1 forces core delivery only
|
|
7
|
+
* for the conversations tool and plugin delivery, which build their sends
|
|
8
|
+
* from text and media; the message tool's own `send`, the one path that
|
|
9
|
+
* carries the parameters, reaches `actions.handleAction`. If OpenClaw adds a
|
|
10
|
+
* parameter bag to this context, this canary fails, and `message.send.text`
|
|
11
|
+
* must then refuse a send that names a collective operation rather than
|
|
12
|
+
* certify it as a multicast.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=core-delivery.types-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-delivery.types-check.js","sourceRoot":"","sources":["../src/core-delivery.types-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -32,13 +32,14 @@ export declare function makeMoltZapChannelConfigJsonSchema(): JSONSchema.JsonSch
|
|
|
32
32
|
* participant Client as HarnessEndpoint
|
|
33
33
|
* Host->>Plugin: start account with its runtime
|
|
34
34
|
* Plugin->>Client: acquire endpoint
|
|
35
|
-
* Client-->>Plugin:
|
|
36
|
-
* Plugin->>Host: submit routed turn
|
|
35
|
+
* Client-->>Plugin: multicast, collective request, result or failure item
|
|
36
|
+
* Plugin->>Host: submit routed turn in the item's fixed form
|
|
37
37
|
* Host->>Host: record session and run agent
|
|
38
38
|
* Host-->>Plugin: final reply withheld
|
|
39
39
|
* Plugin->>Client: acknowledge delivery
|
|
40
|
-
* Host->>Plugin:
|
|
41
|
-
* Plugin->>Client: send
|
|
40
|
+
* Host->>Plugin: message tool send with address, collective or collectiveResponse
|
|
41
|
+
* Plugin->>Client: send the operation or response
|
|
42
|
+
* Plugin-->>Host: tool result with the operation id, or the Client error
|
|
42
43
|
* ```
|
|
43
44
|
* @param deps Optional process-local dependency overrides used by tests.
|
|
44
45
|
* @returns The MoltZap channel plugin.
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,gEAAgE;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAShE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAEjF,OAAO,EAKL,KAAK,eAAe,EAOrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAKL,UAAU,EAEV,MAAM,EAGP,MAAM,QAAQ,CAAC;AAIhB,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,kCAAkC,CAAC;AA2C1C,QAAA,MAAM,oBAAoB;;;;EAIxB,CAAC;AAEH,oEAAoE;AACpE,KAAK,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA8DtE,UAAU,wBAAwB;IAChC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,KACpB,eAAe,GAAG,SAAS,CAAC;CAClC;AAuFD;;;;GAIG;AACH,wBAAgB,kCAAkC,+BAEjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,GAAE,wBAA6B,GAClC,aAAa,CAAC,cAAc,CAAC,CAiB/B;AA23BD,QAAA,MAAM,MAAM,EAAE,wBAAwB,GACpC,QAAQ,CAAC,IAAI,CAAC,wBAAwB,EAAE,IAAI,GAAG,UAAU,CAAC,CAMxD,CAAC;eAGU,MAAM"}
|
package/dist/plugin.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/** @file OpenClaw plugin that registers the MoltZap channel. */
|
|
2
|
-
import { acquireHarnessEndpoint, MessageAddressInput, SendInput, } from "@moltzap/client";
|
|
3
|
-
import { Config, ConfigError, Data, Effect, JSONSchema, Option, Schema, Stream, } from "effect";
|
|
2
|
+
import { acquireHarnessEndpoint, CollectiveOperation, CollectiveResponse, MessageAddressInput, SendInput, } from "@moltzap/client";
|
|
3
|
+
import { Config, ConfigError, Data, Effect, JSONSchema, Option, Schema, Stream, Struct, } from "effect";
|
|
4
|
+
import { absurd } from "effect/Function";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { jsonResult } from "openclaw/plugin-sdk/channel-actions";
|
|
5
7
|
import { createChannelPluginBase, defineChannelPluginEntry, } from "openclaw/plugin-sdk/channel-core";
|
|
6
8
|
import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, waitUntilAbort, } from "openclaw/plugin-sdk/channel-outbound";
|
|
9
|
+
import { Type } from "typebox";
|
|
7
10
|
const CHANNEL_ID = "moltzap";
|
|
8
11
|
const TARGET_HINT = 'Use an explicit "agent:<name>" or "group:<member>,<member>,..." address';
|
|
9
12
|
const INBOUND_LOG_PREVIEW_CHARS = 80;
|
|
13
|
+
/**
|
|
14
|
+
* Experiment control for evaluations that compare agents with and without
|
|
15
|
+
* collective operations. It is not a product setting: do not set it in
|
|
16
|
+
* production, and it may be removed without notice. When true, the message
|
|
17
|
+
* tool omits the `collective` and `collectiveResponse` parameters and a send
|
|
18
|
+
* carrying either fails with {@link OpenClawCollectivesUnavailableError}.
|
|
19
|
+
*/
|
|
20
|
+
const HIDE_COLLECTIVES_VARIABLE = "MOLTZAP_EXPERIMENT_HIDE_COLLECTIVES";
|
|
10
21
|
const moltZapAccountSchema = Schema.Struct({
|
|
11
22
|
id: Schema.String,
|
|
12
23
|
enabled: Schema.optional(Schema.Boolean),
|
|
@@ -20,10 +31,7 @@ const moltZapOpenClawConfigSchema = Schema.Struct({
|
|
|
20
31
|
});
|
|
21
32
|
class OpenClawInboundError extends Data.TaggedError("OpenClawInboundError") {
|
|
22
33
|
get message() {
|
|
23
|
-
|
|
24
|
-
? this.accountId
|
|
25
|
-
: `${this.accountId}/${this.postId}`;
|
|
26
|
-
return `MoltZap inbound delivery failed for ${identity}: ${this.reason}: ${this.detail}`;
|
|
34
|
+
return `MoltZap inbound delivery failed for ${this.accountId}/${this.turnId}: ${this.reason}: ${this.detail}`;
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
37
|
class OpenClawOutboundError extends Data.TaggedError("OpenClawOutboundError") {
|
|
@@ -31,9 +39,19 @@ class OpenClawOutboundError extends Data.TaggedError("OpenClawOutboundError") {
|
|
|
31
39
|
return `MoltZap message delivery failed for account ${this.accountId}: ${this.reason}`;
|
|
32
40
|
}
|
|
33
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* A message tool send carrying `collective` or `collectiveResponse` while
|
|
44
|
+
* {@link HIDE_COLLECTIVES_VARIABLE} is true. The tool does not offer either
|
|
45
|
+
* parameter then, so the message tells the model to send without it.
|
|
46
|
+
*/
|
|
47
|
+
class OpenClawCollectivesUnavailableError extends Data.TaggedError("OpenClawCollectivesUnavailableError") {
|
|
48
|
+
get message() {
|
|
49
|
+
return `MoltZap collective operations are not available for account ${this.accountId}; send the message without collective or collectiveResponse`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
34
52
|
class OpenClawConfigurationError extends Data.TaggedError("OpenClawConfigurationError") {
|
|
35
53
|
get message() {
|
|
36
|
-
return `MoltZap configuration
|
|
54
|
+
return `MoltZap configuration is invalid: ${this.detail}`;
|
|
37
55
|
}
|
|
38
56
|
}
|
|
39
57
|
class OpenClawRuntimeError extends Data.TaggedError("OpenClawRuntimeError") {
|
|
@@ -42,6 +60,26 @@ class OpenClawRuntimeError extends Data.TaggedError("OpenClawRuntimeError") {
|
|
|
42
60
|
}
|
|
43
61
|
}
|
|
44
62
|
const isMessageAddressInput = Schema.is(MessageAddressInput);
|
|
63
|
+
/**
|
|
64
|
+
* The `collective` parameter MoltZap adds to the message tool's `send`
|
|
65
|
+
* action. Its JSON Schema comes from the Client's `CollectiveOperation`, so
|
|
66
|
+
* the tool accepts exactly the operations the endpoint does; TypeBox marks it
|
|
67
|
+
* optional because a `send` without it is a multicast. The description names
|
|
68
|
+
* OpenClaw's own `message` parameter, which carries the question.
|
|
69
|
+
*/
|
|
70
|
+
const collectiveParameter = Type.Optional(Type.Unsafe({
|
|
71
|
+
...Struct.omit(JSONSchema.make(CollectiveOperation), "$schema"),
|
|
72
|
+
description: 'The MoltZap collective operation; the moltzap-collectives skill describes each. Omit it for a multicast: message reaches every agent the target names. {"op":"gather","deadline":<seconds>,"requestedSchema":<form>} sends message as a question to each member of the target, privately; when every member has answered or the deadline passes, only you receive one result turn listing each member\'s answer, decline, cancel or no answer. {"op":"all_gather","deadline":<seconds>,"requestedSchema":<form>} needs a group target: every member receives the question in the group, no one sees another\'s answer before the close, and every member, you included, receives the same result turn. deadline is a whole number of seconds from now, 1 to 2592000 (30 days). requestedSchema is a flat MCP form: {"type":"object","properties":{...},"required":[...]} of string, number, integer, boolean, string-enum or string-enum-array fields. The tool result carries the operation\'s operationId.',
|
|
73
|
+
}));
|
|
74
|
+
/**
|
|
75
|
+
* The `collectiveResponse` parameter: one answer to a collective request
|
|
76
|
+
* turn. Its JSON Schema comes from the Client's `CollectiveResponse`; the
|
|
77
|
+
* description says what OpenClaw needs beyond it.
|
|
78
|
+
*/
|
|
79
|
+
const collectiveResponseParameter = Type.Optional(Type.Unsafe({
|
|
80
|
+
...Struct.omit(JSONSchema.make(CollectiveResponse), "$schema"),
|
|
81
|
+
description: 'Answer a MoltZap collective request turn once; the moltzap-collectives skill describes it. {"id":<request id>,"action":"accept","content":{...}} with content matching the request\'s form, or {"id":<request id>,"action":"decline"} or "cancel" without content. The answer goes back where the request came from, whatever target says. message is not sent, but OpenClaw requires a short non-empty one. An answer that does not match the form fails naming the fields; fix them and send again.',
|
|
82
|
+
}));
|
|
45
83
|
/**
|
|
46
84
|
* Returns the manifest schema for one MoltZap channel configuration.
|
|
47
85
|
* @returns The JSON Schema embedded in the OpenClaw plugin manifest.
|
|
@@ -63,13 +101,14 @@ export function makeMoltZapChannelConfigJsonSchema() {
|
|
|
63
101
|
* participant Client as HarnessEndpoint
|
|
64
102
|
* Host->>Plugin: start account with its runtime
|
|
65
103
|
* Plugin->>Client: acquire endpoint
|
|
66
|
-
* Client-->>Plugin:
|
|
67
|
-
* Plugin->>Host: submit routed turn
|
|
104
|
+
* Client-->>Plugin: multicast, collective request, result or failure item
|
|
105
|
+
* Plugin->>Host: submit routed turn in the item's fixed form
|
|
68
106
|
* Host->>Host: record session and run agent
|
|
69
107
|
* Host-->>Plugin: final reply withheld
|
|
70
108
|
* Plugin->>Client: acknowledge delivery
|
|
71
|
-
* Host->>Plugin:
|
|
72
|
-
* Plugin->>Client: send
|
|
109
|
+
* Host->>Plugin: message tool send with address, collective or collectiveResponse
|
|
110
|
+
* Plugin->>Client: send the operation or response
|
|
111
|
+
* Plugin-->>Host: tool result with the operation id, or the Client error
|
|
73
112
|
* ```
|
|
74
113
|
* @param deps Optional process-local dependency overrides used by tests.
|
|
75
114
|
* @returns The MoltZap channel plugin.
|
|
@@ -88,6 +127,7 @@ export function createMoltzapChannelPlugin(deps = {}) {
|
|
|
88
127
|
gateway: {
|
|
89
128
|
startAccount: (ctx) => startAccountConnection(ctx, connectedAccount, deps),
|
|
90
129
|
},
|
|
130
|
+
actions: createMessageActions(connectedAccount),
|
|
91
131
|
message: createMessageSection(connectedAccount),
|
|
92
132
|
};
|
|
93
133
|
}
|
|
@@ -138,6 +178,75 @@ function createConfigSection() {
|
|
|
138
178
|
},
|
|
139
179
|
};
|
|
140
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* The message tool's `send` action for MoltZap. OpenClaw routes every model
|
|
183
|
+
* `send` here because the adapter defines no prepared payload or gateway
|
|
184
|
+
* execution mode, so the `collective` parameter reaches the endpoint
|
|
185
|
+
* unchanged. `message.send.text` remains for the sends OpenClaw's core makes
|
|
186
|
+
* itself. The tool offers `send` without the collective parameters while
|
|
187
|
+
* {@link HIDE_COLLECTIVES_VARIABLE} is true or unreadable.
|
|
188
|
+
* @param connectedAccount The account whose endpoint performs the operation.
|
|
189
|
+
* @returns The action adapter registered on the channel plugin.
|
|
190
|
+
*/
|
|
191
|
+
function createMessageActions(connectedAccount) {
|
|
192
|
+
return {
|
|
193
|
+
describeMessageTool: () => ({
|
|
194
|
+
actions: ["send"],
|
|
195
|
+
...(Effect.runSync(experimentHidesCollectives().pipe(Effect.orElseSucceed(() => true)))
|
|
196
|
+
? {}
|
|
197
|
+
: {
|
|
198
|
+
schema: {
|
|
199
|
+
properties: {
|
|
200
|
+
collective: collectiveParameter,
|
|
201
|
+
collectiveResponse: collectiveResponseParameter,
|
|
202
|
+
},
|
|
203
|
+
actions: ["send"],
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
}),
|
|
207
|
+
supportsAction: ({ action }) => action === "send",
|
|
208
|
+
handleAction: (ctx) => runHostPromise(handleMessageAction(connectedAccount, ctx)),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function handleMessageAction(connectedAccount, ctx) {
|
|
212
|
+
if (ctx.action !== "send") {
|
|
213
|
+
return Effect.fail(new OpenClawOutboundError({
|
|
214
|
+
reason: "unsupported-action",
|
|
215
|
+
accountId: accountLabel(ctx.accountId),
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
const send = {
|
|
219
|
+
accountId: ctx.accountId,
|
|
220
|
+
to: ctx.params.to,
|
|
221
|
+
text: ctx.params.message,
|
|
222
|
+
collective: ctx.params.collective,
|
|
223
|
+
collectiveResponse: ctx.params.collectiveResponse,
|
|
224
|
+
};
|
|
225
|
+
return refuseHiddenCollectives(send).pipe(Effect.andThen(() => sendOperation(connectedAccount, send)), Effect.map(({ input, result }) => jsonResult({
|
|
226
|
+
ok: true,
|
|
227
|
+
...("to" in input ? { to: input.to } : {}),
|
|
228
|
+
...result,
|
|
229
|
+
})));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Refuse a send carrying a collective parameter while the experiment hides
|
|
233
|
+
* collectives. The switch is read only for such a send, so an unreadable
|
|
234
|
+
* value fails it with a configuration error naming the variable and leaves
|
|
235
|
+
* plain sends unaffected.
|
|
236
|
+
*/
|
|
237
|
+
function refuseHiddenCollectives(send) {
|
|
238
|
+
if (send.collective === undefined && send.collectiveResponse === undefined) {
|
|
239
|
+
return Effect.void;
|
|
240
|
+
}
|
|
241
|
+
return experimentHidesCollectives().pipe(Effect.flatMap((hidden) => hidden
|
|
242
|
+
? Effect.fail(new OpenClawCollectivesUnavailableError({
|
|
243
|
+
accountId: accountLabel(send.accountId),
|
|
244
|
+
}))
|
|
245
|
+
: Effect.void));
|
|
246
|
+
}
|
|
247
|
+
function experimentHidesCollectives() {
|
|
248
|
+
return Config.boolean(HIDE_COLLECTIVES_VARIABLE).pipe(Config.withDefault(false));
|
|
249
|
+
}
|
|
141
250
|
function createMessageSection(connectedAccount) {
|
|
142
251
|
return defineChannelMessageAdapter({
|
|
143
252
|
id: CHANNEL_ID,
|
|
@@ -283,12 +392,107 @@ function reportConnected(ctx) {
|
|
|
283
392
|
});
|
|
284
393
|
}
|
|
285
394
|
function handleInboundDelivery(ctx, runtime, delivery) {
|
|
286
|
-
|
|
395
|
+
const turn = inboundItemTurn(delivery.item);
|
|
396
|
+
return logInbound(ctx, turn).pipe(Effect.zipRight(runOpenClawTurn(ctx, runtime, turn)), Effect.zipRight(delivery.acknowledge));
|
|
287
397
|
}
|
|
288
|
-
|
|
398
|
+
/** The sender OpenClaw records for a turn the endpoint itself emitted. */
|
|
399
|
+
const COLLECTIVE_SENDER_NAME = "MoltZap collective";
|
|
400
|
+
/**
|
|
401
|
+
* Render one inbound item as the turn its kind defines. Each kind has one
|
|
402
|
+
* fixed form, the same for every agent. A request belongs to the conversation
|
|
403
|
+
* it arrived in: the requester's for a gather, the group's for an all_gather.
|
|
404
|
+
* A result or a failure is attributed to the collective, not to any member,
|
|
405
|
+
* and belongs to the conversation its operation addressed.
|
|
406
|
+
* @param item The item the endpoint delivered.
|
|
407
|
+
* @returns The turn OpenClaw runs.
|
|
408
|
+
*/
|
|
409
|
+
function inboundItemTurn(item) {
|
|
410
|
+
switch (item.kind) {
|
|
411
|
+
case "multicast":
|
|
412
|
+
return multicastTurn(item.message);
|
|
413
|
+
case "collectiveRequest":
|
|
414
|
+
return addressedTurn(item.postId, item.to, agentSender(item.from), renderCollectiveRequest(item));
|
|
415
|
+
case "collectiveResult":
|
|
416
|
+
return collectiveTurn(`${item.id}:result`, item.to, renderCollectiveResult(item));
|
|
417
|
+
case "operationFailed":
|
|
418
|
+
return collectiveTurn(`${item.id}:failed`, item.to, `MoltZap operation failed: ${item.error}`);
|
|
419
|
+
default:
|
|
420
|
+
return absurd(item);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function multicastTurn(message) {
|
|
424
|
+
const base = {
|
|
425
|
+
id: message.postId,
|
|
426
|
+
address: message.address,
|
|
427
|
+
sender: agentSender(message.sender),
|
|
428
|
+
body: renderContent(message.content),
|
|
429
|
+
};
|
|
430
|
+
return message.kind === "group"
|
|
431
|
+
? { ...base, kind: "group", members: message.members }
|
|
432
|
+
: { ...base, kind: "direct" };
|
|
433
|
+
}
|
|
434
|
+
function collectiveTurn(id, address, body) {
|
|
435
|
+
return addressedTurn(id, address, { id: `collective:${id}`, name: COLLECTIVE_SENDER_NAME }, body);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* A turn in the conversation `address` names: a group address makes a group
|
|
439
|
+
* turn listing its members.
|
|
440
|
+
*/
|
|
441
|
+
function addressedTurn(id, address, sender, body) {
|
|
442
|
+
const kind = address.startsWith("group:") ? "group" : "direct";
|
|
443
|
+
return {
|
|
444
|
+
id,
|
|
445
|
+
kind,
|
|
446
|
+
address,
|
|
447
|
+
sender,
|
|
448
|
+
...(kind === "group"
|
|
449
|
+
? {
|
|
450
|
+
members: address
|
|
451
|
+
.slice("group:".length)
|
|
452
|
+
.split(",")
|
|
453
|
+
.map((name) => `agent:${name}`),
|
|
454
|
+
}
|
|
455
|
+
: {}),
|
|
456
|
+
body,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function agentSender(address) {
|
|
460
|
+
return { id: address, name: address.slice("agent:".length) };
|
|
461
|
+
}
|
|
462
|
+
function renderCollectiveRequest(item) {
|
|
463
|
+
const deadline = new Date(item.deadlineAt).toISOString();
|
|
464
|
+
return [
|
|
465
|
+
`MoltZap collective request ${item.id} from ${item.from}, open until ${deadline}.`,
|
|
466
|
+
`Question: ${item.question}`,
|
|
467
|
+
`Answer form (requestedSchema): ${JSON.stringify(item.requestedSchema)}`,
|
|
468
|
+
`Answer once with the message tool's send action and collectiveResponse {"id":"${item.id}","action":"accept","content":{...}} matching the form, or {"id":"${item.id}","action":"decline"}.`,
|
|
469
|
+
].join("\n");
|
|
470
|
+
}
|
|
471
|
+
function renderCollectiveResult(item) {
|
|
472
|
+
return [
|
|
473
|
+
`MoltZap collective result ${item.id} for the question sent to ${item.to}: ${item.question}`,
|
|
474
|
+
...item.outcomes.map(({ member, outcome }) => `- ${member}: ${renderOutcome(outcome)}`),
|
|
475
|
+
].join("\n");
|
|
476
|
+
}
|
|
477
|
+
function renderOutcome(outcome) {
|
|
478
|
+
switch (outcome.kind) {
|
|
479
|
+
case "answered":
|
|
480
|
+
return `answered ${JSON.stringify(outcome.content)}`;
|
|
481
|
+
case "declined":
|
|
482
|
+
return "declined";
|
|
483
|
+
case "cancelled":
|
|
484
|
+
return "cancelled";
|
|
485
|
+
case "invalid":
|
|
486
|
+
return `answered outside the form (${outcome.reason})`;
|
|
487
|
+
case "no-answer":
|
|
488
|
+
return "no answer by the deadline";
|
|
489
|
+
default:
|
|
490
|
+
return absurd(outcome);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function logInbound(ctx, turn) {
|
|
289
494
|
return Effect.sync(() => {
|
|
290
|
-
|
|
291
|
-
ctx.log?.info?.(`MoltZap: inbound from ${message.sender}: ${body.slice(0, INBOUND_LOG_PREVIEW_CHARS)}`);
|
|
495
|
+
ctx.log?.info?.(`MoltZap: inbound from ${turn.sender.id}: ${turn.body.slice(0, INBOUND_LOG_PREVIEW_CHARS)}`);
|
|
292
496
|
ctx.setStatus({
|
|
293
497
|
...ctx.getStatus(),
|
|
294
498
|
accountId: ctx.accountId,
|
|
@@ -297,35 +501,34 @@ function logInbound(ctx, message) {
|
|
|
297
501
|
});
|
|
298
502
|
});
|
|
299
503
|
}
|
|
300
|
-
function runOpenClawTurn(ctx, runtime,
|
|
301
|
-
const body = renderContent(message.content);
|
|
504
|
+
function runOpenClawTurn(ctx, runtime, turn) {
|
|
302
505
|
return Effect.tryPromise({
|
|
303
506
|
try: () => runtime.inbound.run({
|
|
304
507
|
channel: CHANNEL_ID,
|
|
305
508
|
accountId: ctx.accountId,
|
|
306
|
-
raw: {
|
|
509
|
+
raw: { turn },
|
|
307
510
|
adapter: {
|
|
308
511
|
ingest: () => ({
|
|
309
|
-
id:
|
|
310
|
-
rawText: body,
|
|
311
|
-
textForAgent: body,
|
|
312
|
-
textForCommands: body,
|
|
313
|
-
raw:
|
|
512
|
+
id: turn.id,
|
|
513
|
+
rawText: turn.body,
|
|
514
|
+
textForAgent: turn.body,
|
|
515
|
+
textForCommands: turn.body,
|
|
516
|
+
raw: turn,
|
|
314
517
|
}),
|
|
315
|
-
resolveTurn: () => buildRoutedTurnPlan({ ctx, runtime,
|
|
518
|
+
resolveTurn: () => buildRoutedTurnPlan({ ctx, runtime, turn }),
|
|
316
519
|
},
|
|
317
520
|
}),
|
|
318
521
|
catch: (cause) => new OpenClawInboundError({
|
|
319
522
|
reason: "turn-failed",
|
|
320
523
|
accountId: ctx.accountId,
|
|
321
|
-
|
|
524
|
+
turnId: turn.id,
|
|
322
525
|
detail: String(cause),
|
|
323
526
|
}),
|
|
324
527
|
});
|
|
325
528
|
}
|
|
326
529
|
function buildRoutedTurnPlan(input) {
|
|
327
|
-
const { ctx,
|
|
328
|
-
const peer = inboundRoutePeer(
|
|
530
|
+
const { ctx, turn, runtime } = input;
|
|
531
|
+
const peer = inboundRoutePeer(turn);
|
|
329
532
|
const route = runtime.routing.resolveAgentRoute({
|
|
330
533
|
cfg: ctx.cfg,
|
|
331
534
|
channel: CHANNEL_ID,
|
|
@@ -351,24 +554,24 @@ function buildRoutedTurnPlan(input) {
|
|
|
351
554
|
updateLastRoute: {
|
|
352
555
|
sessionKey,
|
|
353
556
|
channel: CHANNEL_ID,
|
|
354
|
-
to:
|
|
557
|
+
to: turn.address,
|
|
355
558
|
accountId: ctx.accountId,
|
|
356
559
|
},
|
|
357
560
|
},
|
|
358
|
-
messageId:
|
|
561
|
+
messageId: turn.id,
|
|
359
562
|
};
|
|
360
563
|
}
|
|
361
564
|
function buildInboundContext(input, route, sessionKey) {
|
|
362
|
-
const {
|
|
565
|
+
const { ctx, turn, runtime } = input;
|
|
363
566
|
return runtime.inbound.buildContext({
|
|
364
567
|
channel: CHANNEL_ID,
|
|
365
568
|
accountId: ctx.accountId,
|
|
366
569
|
provider: CHANNEL_ID,
|
|
367
570
|
surface: CHANNEL_ID,
|
|
368
|
-
messageId:
|
|
369
|
-
from:
|
|
370
|
-
sender: inboundSenderFacts(
|
|
371
|
-
conversation: inboundConversationFacts(
|
|
571
|
+
messageId: turn.id,
|
|
572
|
+
from: turn.sender.id,
|
|
573
|
+
sender: inboundSenderFacts(turn),
|
|
574
|
+
conversation: inboundConversationFacts(turn),
|
|
372
575
|
route: {
|
|
373
576
|
agentId: route.agentId,
|
|
374
577
|
accountId: ctx.accountId,
|
|
@@ -377,60 +580,56 @@ function buildInboundContext(input, route, sessionKey) {
|
|
|
377
580
|
persistedSessionKey: sessionKey,
|
|
378
581
|
mainSessionKey: route.mainSessionKey,
|
|
379
582
|
},
|
|
380
|
-
reply: inboundReplyFacts(
|
|
583
|
+
reply: inboundReplyFacts(turn),
|
|
381
584
|
message: {
|
|
382
|
-
body,
|
|
383
|
-
rawBody: body,
|
|
384
|
-
bodyForAgent: body,
|
|
385
|
-
commandBody: body,
|
|
585
|
+
body: turn.body,
|
|
586
|
+
rawBody: turn.body,
|
|
587
|
+
bodyForAgent: turn.body,
|
|
588
|
+
commandBody: turn.body,
|
|
386
589
|
},
|
|
387
|
-
extra: inboundGroupFacts(
|
|
590
|
+
extra: inboundGroupFacts(turn),
|
|
388
591
|
});
|
|
389
592
|
}
|
|
390
593
|
/**
|
|
391
|
-
* Every MoltZap sender is another principal's agent
|
|
392
|
-
* as a bot. OpenClaw records that as the
|
|
393
|
-
* and transcript metadata; it does not
|
|
394
|
-
* the sender's text as instructions.
|
|
594
|
+
* Every MoltZap sender is another principal's agent or the collective layer,
|
|
595
|
+
* so the sender is marked as a bot. OpenClaw records that as the
|
|
596
|
+
* participant's `senderKind` in session and transcript metadata; it does not
|
|
597
|
+
* change routing, reply mode, or admit the sender's text as instructions.
|
|
395
598
|
*/
|
|
396
|
-
function inboundSenderFacts(
|
|
397
|
-
return {
|
|
398
|
-
id: message.sender,
|
|
399
|
-
name: message.sender.slice("agent:".length),
|
|
400
|
-
isBot: true,
|
|
401
|
-
};
|
|
599
|
+
function inboundSenderFacts(turn) {
|
|
600
|
+
return { ...turn.sender, isBot: true };
|
|
402
601
|
}
|
|
403
|
-
function inboundConversationFacts(
|
|
404
|
-
const routePeer = inboundRoutePeer(
|
|
602
|
+
function inboundConversationFacts(turn) {
|
|
603
|
+
const routePeer = inboundRoutePeer(turn);
|
|
405
604
|
return {
|
|
406
|
-
kind:
|
|
407
|
-
id:
|
|
408
|
-
label:
|
|
605
|
+
kind: turn.kind,
|
|
606
|
+
id: turn.address,
|
|
607
|
+
label: turn.address,
|
|
409
608
|
routePeer,
|
|
410
609
|
};
|
|
411
610
|
}
|
|
412
|
-
function inboundRoutePeer(
|
|
413
|
-
const prefix =
|
|
611
|
+
function inboundRoutePeer(turn) {
|
|
612
|
+
const prefix = turn.kind === "group" ? "group:" : "agent:";
|
|
414
613
|
return {
|
|
415
|
-
kind:
|
|
416
|
-
id:
|
|
614
|
+
kind: turn.kind,
|
|
615
|
+
id: turn.address.slice(prefix.length),
|
|
417
616
|
};
|
|
418
617
|
}
|
|
419
|
-
function inboundReplyFacts(
|
|
618
|
+
function inboundReplyFacts(turn) {
|
|
420
619
|
return {
|
|
421
|
-
to:
|
|
422
|
-
originatingTo:
|
|
423
|
-
replyTarget:
|
|
424
|
-
deliveryTarget:
|
|
620
|
+
to: turn.address,
|
|
621
|
+
originatingTo: turn.address,
|
|
622
|
+
replyTarget: turn.address,
|
|
623
|
+
deliveryTarget: turn.address,
|
|
425
624
|
};
|
|
426
625
|
}
|
|
427
|
-
function inboundGroupFacts(
|
|
428
|
-
return
|
|
429
|
-
?
|
|
430
|
-
:
|
|
626
|
+
function inboundGroupFacts(turn) {
|
|
627
|
+
return turn.members === undefined
|
|
628
|
+
? undefined
|
|
629
|
+
: { GroupMembers: turn.members.join(",") };
|
|
431
630
|
}
|
|
432
631
|
/**
|
|
433
|
-
* Final assistant text never becomes a MoltZap post.
|
|
632
|
+
* Final assistant text never becomes a MoltZap post. A launcher's OpenClaw
|
|
434
633
|
* configuration selects tool-only visible replies, and this callback withholds
|
|
435
634
|
* anything that still reaches it, so the `message` tool with an explicit
|
|
436
635
|
* target is the only send path. Non-empty text reaching here means OpenClaw
|
|
@@ -462,15 +661,22 @@ function removeConnectedEndpoint(connectedAccount, accountId, active) {
|
|
|
462
661
|
});
|
|
463
662
|
}
|
|
464
663
|
function sendOpenClawText(connectedAccount, ctx) {
|
|
465
|
-
return
|
|
664
|
+
return sendOperation(connectedAccount, {
|
|
466
665
|
accountId: ctx.accountId,
|
|
467
|
-
messageId: randomUUID(),
|
|
468
666
|
text: ctx.text,
|
|
469
667
|
to: ctx.to,
|
|
470
|
-
});
|
|
668
|
+
}).pipe(Effect.as(makeMessageSendResult(randomUUID())));
|
|
471
669
|
}
|
|
472
|
-
|
|
473
|
-
|
|
670
|
+
/**
|
|
671
|
+
* Perform one host send as one Client operation or collective response. A
|
|
672
|
+
* refused send fails with the Client's error, whose message OpenClaw returns
|
|
673
|
+
* to the model as the tool error.
|
|
674
|
+
* @param connectedAccount The account whose endpoint performs the operation.
|
|
675
|
+
* @param params The host's account, address, text, operation and response.
|
|
676
|
+
* @returns The validated input and the endpoint's result.
|
|
677
|
+
*/
|
|
678
|
+
function sendOperation(connectedAccount, params) {
|
|
679
|
+
const accountId = accountLabel(params.accountId);
|
|
474
680
|
const endpoint = connectedEndpoint(connectedAccount, params.accountId);
|
|
475
681
|
if (endpoint === undefined) {
|
|
476
682
|
return Effect.fail(new OpenClawOutboundError({
|
|
@@ -478,19 +684,42 @@ function sendAddressedText(connectedAccount, params) {
|
|
|
478
684
|
accountId,
|
|
479
685
|
}));
|
|
480
686
|
}
|
|
481
|
-
return decodeSendInput(params, accountId).pipe(Effect.flatMap((input) => endpoint.send(input)
|
|
687
|
+
return decodeSendInput(params, accountId).pipe(Effect.flatMap((input) => endpoint.send(input).pipe(Effect.map((result) => ({ input, result })))));
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Name the host's account in an outbound error, including when the host sent
|
|
691
|
+
* none.
|
|
692
|
+
* @param accountId The account id the host supplied, if any.
|
|
693
|
+
* @returns The trimmed id, or a placeholder the error message can print.
|
|
694
|
+
*/
|
|
695
|
+
function accountLabel(accountId) {
|
|
696
|
+
return accountId?.trim() ?? "(unspecified)";
|
|
482
697
|
}
|
|
698
|
+
/**
|
|
699
|
+
* Decode one host send. A `collectiveResponse` is sent without the target or
|
|
700
|
+
* text: its endpoint addresses the requester, and OpenClaw requires text on
|
|
701
|
+
* every send even though a response carries none.
|
|
702
|
+
*/
|
|
483
703
|
function decodeSendInput(params, accountId) {
|
|
484
|
-
|
|
704
|
+
if (params.collectiveResponse !== undefined) {
|
|
705
|
+
return decodeOrFail({ collectiveResponse: params.collectiveResponse }, accountId);
|
|
706
|
+
}
|
|
707
|
+
if (!isMessageAddressInput(params.to)) {
|
|
708
|
+
return Effect.fail(new OpenClawOutboundError({ reason: "invalid-address", accountId }));
|
|
709
|
+
}
|
|
710
|
+
return decodeOrFail({
|
|
485
711
|
to: params.to,
|
|
486
|
-
|
|
487
|
-
|
|
712
|
+
text: params.text,
|
|
713
|
+
...(params.collective === undefined
|
|
714
|
+
? {}
|
|
715
|
+
: { collective: params.collective }),
|
|
716
|
+
}, accountId);
|
|
717
|
+
}
|
|
718
|
+
function decodeOrFail(value, accountId) {
|
|
719
|
+
const decoded = Schema.decodeUnknownOption(SendInput)(value);
|
|
488
720
|
return Option.isSome(decoded)
|
|
489
721
|
? Effect.succeed(decoded.value)
|
|
490
|
-
: Effect.fail(new OpenClawOutboundError({
|
|
491
|
-
reason: "invalid-address",
|
|
492
|
-
accountId,
|
|
493
|
-
}));
|
|
722
|
+
: Effect.fail(new OpenClawOutboundError({ reason: "invalid-operation", accountId }));
|
|
494
723
|
}
|
|
495
724
|
function makeMessageSendResult(messageId) {
|
|
496
725
|
return {
|
|
@@ -520,8 +749,9 @@ function runHostPromise(effect) {
|
|
|
520
749
|
function hostPromiseError(error) {
|
|
521
750
|
if (ConfigError.isConfigError(error)) {
|
|
522
751
|
return new OpenClawConfigurationError({
|
|
523
|
-
|
|
524
|
-
|
|
752
|
+
detail: ConfigError.isInvalidData(error)
|
|
753
|
+
? `${error.path.join(".")}: ${error.message}`
|
|
754
|
+
: error.message,
|
|
525
755
|
});
|
|
526
756
|
}
|
|
527
757
|
return error;
|