Skip to content

Commit 8bfabd6

Browse files
committed
feat: add channel message lifecycle sdk
1 parent 411211c commit 8bfabd6

49 files changed

Lines changed: 4994 additions & 206 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,14 @@
798798
"types": "./dist/plugin-sdk/channel-lifecycle.d.ts",
799799
"default": "./dist/plugin-sdk/channel-lifecycle.js"
800800
},
801+
"./plugin-sdk/channel-message": {
802+
"types": "./dist/plugin-sdk/channel-message.d.ts",
803+
"default": "./dist/plugin-sdk/channel-message.js"
804+
},
805+
"./plugin-sdk/channel-message-runtime": {
806+
"types": "./dist/plugin-sdk/channel-message-runtime.d.ts",
807+
"default": "./dist/plugin-sdk/channel-message-runtime.js"
808+
},
801809
"./plugin-sdk/channel-pairing": {
802810
"types": "./dist/plugin-sdk/channel-pairing.d.ts",
803811
"default": "./dist/plugin-sdk/channel-pairing.js"

scripts/lib/plugin-sdk-entrypoints.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@
176176
"channel-location",
177177
"channel-mention-gating",
178178
"channel-lifecycle",
179+
"channel-message",
180+
"channel-message-runtime",
179181
"channel-pairing",
180182
"channel-pairing-paths",
181183
"channel-policy",

src/agents/pi-embedded-runner/run/attempt-system-prompt.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { describe, expect, it } from "vitest";
2-
import { buildAttemptSystemPrompt } from "./attempt-system-prompt.js";
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
let buildAttemptSystemPrompt: typeof import("./attempt-system-prompt.js").buildAttemptSystemPrompt;
4+
5+
beforeEach(async () => {
6+
vi.resetModules();
7+
vi.doUnmock("../system-prompt.js");
8+
({ buildAttemptSystemPrompt } = await import("./attempt-system-prompt.js"));
9+
});
310

411
const baseProviderTransform = {
512
provider: "openai",
Lines changed: 36 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1-
export type DraftPreviewFinalizerDraft<TId> = {
2-
flush: () => Promise<void>;
3-
id: () => TId | undefined;
4-
seal?: () => Promise<void>;
5-
discardPending?: () => Promise<void>;
6-
clear: () => Promise<void>;
7-
};
1+
import {
2+
deliverFinalizableLivePreview,
3+
type LivePreviewFinalizerDraft,
4+
type LivePreviewFinalizerResultKind,
5+
} from "./message/live.js";
86

9-
export type DraftPreviewFinalizerResult =
10-
| "normal-delivered"
11-
| "normal-skipped"
12-
| "preview-finalized";
7+
/**
8+
* @deprecated Use `LivePreviewFinalizerDraft` from `openclaw/plugin-sdk/channel-message`.
9+
*/
10+
export type DraftPreviewFinalizerDraft<TId> = LivePreviewFinalizerDraft<TId>;
1311

12+
/**
13+
* @deprecated Use `LivePreviewFinalizerResult` from `openclaw/plugin-sdk/channel-message`.
14+
*/
15+
export type DraftPreviewFinalizerResult = Exclude<
16+
LivePreviewFinalizerResultKind,
17+
"preview-retained"
18+
>;
19+
20+
/**
21+
* @deprecated Use `deliverFinalizableLivePreview` from `openclaw/plugin-sdk/channel-message`.
22+
*/
1423
export async function deliverFinalizableDraftPreview<TPayload, TId, TEdit>(params: {
1524
kind: "tool" | "block" | "final";
1625
payload: TPayload;
@@ -22,49 +31,21 @@ export async function deliverFinalizableDraftPreview<TPayload, TId, TEdit>(param
2231
onNormalDelivered?: () => Promise<void> | void;
2332
logPreviewEditFailure?: (error: unknown) => void;
2433
}): Promise<DraftPreviewFinalizerResult> {
25-
if (params.kind !== "final" || !params.draft) {
26-
const delivered = await params.deliverNormally(params.payload);
27-
if (delivered === false) {
28-
return "normal-skipped";
29-
}
30-
await params.onNormalDelivered?.();
31-
return "normal-delivered";
32-
}
33-
34-
const edit = params.buildFinalEdit(params.payload);
35-
if (edit !== undefined) {
36-
await params.draft.flush();
37-
const previewId = params.draft.id();
38-
if (previewId !== undefined) {
39-
await params.draft.seal?.();
40-
try {
41-
await params.editFinal(previewId, edit);
42-
await params.onPreviewFinalized?.(previewId);
43-
return "preview-finalized";
44-
} catch (err) {
45-
params.logPreviewEditFailure?.(err);
46-
}
47-
}
48-
}
49-
50-
if (params.draft.discardPending) {
51-
await params.draft.discardPending();
52-
} else {
53-
await params.draft.clear();
54-
}
55-
56-
let delivered = false;
57-
try {
58-
const result = await params.deliverNormally(params.payload);
59-
delivered = result !== false;
60-
if (delivered) {
61-
await params.onNormalDelivered?.();
62-
}
63-
} finally {
64-
if (delivered) {
65-
await params.draft.clear();
66-
}
67-
}
34+
const result = await deliverFinalizableLivePreview({
35+
kind: params.kind,
36+
payload: params.payload,
37+
...(params.draft ? { draft: params.draft } : {}),
38+
buildFinalEdit: params.buildFinalEdit,
39+
editFinal: params.editFinal,
40+
deliverNormally: params.deliverNormally,
41+
onPreviewFinalized: async (id) => {
42+
await params.onPreviewFinalized?.(id);
43+
},
44+
...(params.onNormalDelivered ? { onNormalDelivered: params.onNormalDelivered } : {}),
45+
...(params.logPreviewEditFailure
46+
? { logPreviewEditFailure: params.logPreviewEditFailure }
47+
: {}),
48+
});
6849

69-
return delivered ? "normal-delivered" : "normal-skipped";
50+
return result.kind === "preview-retained" ? "normal-skipped" : result.kind;
7051
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it } from "vitest";
2+
import { deriveDurableFinalDeliveryRequirements } from "./capabilities.js";
3+
4+
describe("deriveDurableFinalDeliveryRequirements", () => {
5+
it("derives the default durable final text and hook requirements", () => {
6+
expect(deriveDurableFinalDeliveryRequirements({ payload: { text: "hello" } })).toEqual({
7+
text: true,
8+
messageSendingHooks: true,
9+
});
10+
});
11+
12+
it("derives payload-dependent delivery requirements", () => {
13+
expect(
14+
deriveDurableFinalDeliveryRequirements({
15+
payload: {
16+
text: "caption",
17+
mediaUrls: ["https://example.com/a.png"],
18+
replyToId: "reply-1",
19+
},
20+
threadId: 42,
21+
silent: true,
22+
payloadTransport: true,
23+
batch: true,
24+
reconcileUnknownSend: true,
25+
afterSendSuccess: true,
26+
afterCommit: true,
27+
}),
28+
).toEqual({
29+
text: true,
30+
media: true,
31+
replyTo: true,
32+
thread: true,
33+
silent: true,
34+
messageSendingHooks: true,
35+
payload: true,
36+
batch: true,
37+
reconcileUnknownSend: true,
38+
afterSendSuccess: true,
39+
afterCommit: true,
40+
});
41+
});
42+
43+
it("applies channel-native extras without recording false requirements", () => {
44+
expect(
45+
deriveDurableFinalDeliveryRequirements({
46+
payload: { text: "hello" },
47+
extraCapabilities: {
48+
nativeQuote: false,
49+
thread: true,
50+
},
51+
}),
52+
).toEqual({
53+
text: true,
54+
thread: true,
55+
messageSendingHooks: true,
56+
});
57+
});
58+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type {
2+
DeriveDurableFinalDeliveryRequirementsParams,
3+
DurableFinalDeliveryCapability,
4+
DurableFinalDeliveryRequirementMap,
5+
} from "./types.js";
6+
7+
function hasMediaPayload(
8+
payload: DeriveDurableFinalDeliveryRequirementsParams["payload"],
9+
): boolean {
10+
if (payload.mediaUrl?.trim()) {
11+
return true;
12+
}
13+
return (
14+
Array.isArray(payload.mediaUrls) &&
15+
payload.mediaUrls.some((url) => typeof url === "string" && url.trim().length > 0)
16+
);
17+
}
18+
19+
function setRequired(
20+
requirements: DurableFinalDeliveryRequirementMap,
21+
capability: DurableFinalDeliveryCapability,
22+
required: boolean | undefined,
23+
): void {
24+
if (required === true) {
25+
requirements[capability] = true;
26+
}
27+
}
28+
29+
export function deriveDurableFinalDeliveryRequirements(
30+
params: DeriveDurableFinalDeliveryRequirementsParams,
31+
): DurableFinalDeliveryRequirementMap {
32+
const requirements: DurableFinalDeliveryRequirementMap = {};
33+
setRequired(requirements, "text", true);
34+
setRequired(requirements, "media", hasMediaPayload(params.payload));
35+
setRequired(
36+
requirements,
37+
"replyTo",
38+
params.replyToId != null || params.payload.replyToId != null,
39+
);
40+
setRequired(requirements, "thread", params.threadId != null);
41+
setRequired(requirements, "silent", params.silent);
42+
setRequired(requirements, "messageSendingHooks", params.messageSendingHooks !== false);
43+
setRequired(requirements, "payload", params.payloadTransport);
44+
setRequired(requirements, "batch", params.batch);
45+
setRequired(requirements, "reconcileUnknownSend", params.reconcileUnknownSend);
46+
setRequired(requirements, "afterSendSuccess", params.afterSendSuccess);
47+
setRequired(requirements, "afterCommit", params.afterCommit);
48+
49+
for (const [capability, required] of Object.entries(params.extraCapabilities ?? {}) as Array<
50+
[DurableFinalDeliveryCapability, boolean | undefined]
51+
>) {
52+
setRequired(requirements, capability, required);
53+
}
54+
55+
return requirements;
56+
}

0 commit comments

Comments
 (0)