@otterware/openclaw-otter 0.2.1 → 0.2.3
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/index.js +25 -19
- package/otter.js +32 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AsyncResource } from "node:async_hooks";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import {
|
|
4
3
|
createChannelPluginBase,
|
|
@@ -6,6 +5,7 @@ import {
|
|
|
6
5
|
defineChannelPluginEntry,
|
|
7
6
|
} from "openclaw/plugin-sdk/channel-core";
|
|
8
7
|
import { toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
|
|
8
|
+
import { runInDetachedAsyncContext } from "openclaw/plugin-sdk/concurrency-runtime";
|
|
9
9
|
import { defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
10
10
|
import { resolveAgentWorkspaceDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
11
11
|
import { dispatchGatewayMethod } from "openclaw/plugin-sdk/gateway-method-runtime";
|
|
@@ -15,23 +15,19 @@ import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guar
|
|
|
15
15
|
import { readWorkspaceFile } from "./files.js";
|
|
16
16
|
import {
|
|
17
17
|
accountOf,
|
|
18
|
-
chatOf,
|
|
19
18
|
chatTarget,
|
|
19
|
+
isNewChat,
|
|
20
20
|
isOwnerConfigured,
|
|
21
21
|
messageOf,
|
|
22
|
-
|
|
22
|
+
newChatKey,
|
|
23
|
+
otterTargetsNote,
|
|
23
24
|
OWNER,
|
|
24
25
|
pointerOf,
|
|
25
26
|
sessionOf,
|
|
27
|
+
targetOf,
|
|
26
28
|
tell,
|
|
27
29
|
} from "./otter.js";
|
|
28
30
|
|
|
29
|
-
/**
|
|
30
|
-
* Where a message sent from Otter starts its run: outside the request that carried it,
|
|
31
|
-
* so the run is the channel's owner's and not that connection's, and outlives it.
|
|
32
|
-
*/
|
|
33
|
-
const receiving = new AsyncResource("otter-inbound");
|
|
34
|
-
|
|
35
31
|
const owner = defineStableChannelIngressIdentity({
|
|
36
32
|
key: "otter-sender",
|
|
37
33
|
normalize: (value) => String(value).trim(),
|
|
@@ -67,13 +63,15 @@ export const otterChannel = createChatChannelPlugin({
|
|
|
67
63
|
messaging: {
|
|
68
64
|
targetResolver: {
|
|
69
65
|
looksLikeId: () => true,
|
|
70
|
-
hint:
|
|
66
|
+
hint: '"chat:<session key>" for one of Otter\'s chats, "new" for a new chat, or "inbox"',
|
|
71
67
|
},
|
|
72
|
-
// A delivery to a chat is kept in its session;
|
|
68
|
+
// A delivery to a chat is kept in its session; to "new", in a chat it starts (the gateway
|
|
69
|
+
// routes each delivery once, and sends it to that session); any other, in the inbox.
|
|
73
70
|
resolveOutboundSessionRoute: ({ agentId, target }) => {
|
|
74
|
-
const
|
|
75
|
-
const sessionKey =
|
|
76
|
-
|
|
71
|
+
const to = targetOf(target);
|
|
72
|
+
const sessionKey = isNewChat(to)
|
|
73
|
+
? newChatKey(agentId, randomUUID())
|
|
74
|
+
: sessionOf(target, agentId);
|
|
77
75
|
return {
|
|
78
76
|
sessionKey,
|
|
79
77
|
baseSessionKey: sessionKey,
|
|
@@ -88,10 +86,7 @@ export const otterChannel = createChatChannelPlugin({
|
|
|
88
86
|
},
|
|
89
87
|
outbound: {
|
|
90
88
|
deliveryMode: "direct",
|
|
91
|
-
resolveTarget: (params) => {
|
|
92
|
-
const chat = chatOf(params?.to);
|
|
93
|
-
return { ok: true, to: chat ? chatTarget(chat) : OTTER_TARGET };
|
|
94
|
-
},
|
|
89
|
+
resolveTarget: (params) => ({ ok: true, to: targetOf(params?.to) }),
|
|
95
90
|
sendText: (ctx) => deliver(ctx),
|
|
96
91
|
sendPayload: (ctx) => deliver(ctx, ctx.payload),
|
|
97
92
|
},
|
|
@@ -161,6 +156,14 @@ export default defineChannelPluginEntry({
|
|
|
161
156
|
description: "Send what your agents want you to see to Otter.",
|
|
162
157
|
plugin: otterChannel,
|
|
163
158
|
registerFull(api) {
|
|
159
|
+
// What Otter's targets are, for a turn in an Otter chat: the Codex harness leaves channels'
|
|
160
|
+
// message-tool hints out of its instructions, but every harness runs this hook.
|
|
161
|
+
api.on("before_prompt_build", (_event, ctx) =>
|
|
162
|
+
ctx?.channel === "otter" || ctx?.messageProvider === "otter"
|
|
163
|
+
? { appendSystemContext: otterTargetsNote(ctx.sessionKey) }
|
|
164
|
+
: undefined,
|
|
165
|
+
);
|
|
166
|
+
|
|
164
167
|
// What you send in an Otter chat, as the gateway's owner: `chat.send`'s run, with the authority
|
|
165
168
|
// a configured channel owner has (an automation another chat made, for one).
|
|
166
169
|
api.registerGatewayMethod(
|
|
@@ -169,7 +172,10 @@ export default defineChannelPluginEntry({
|
|
|
169
172
|
const cfg = context?.getRuntimeConfig?.() ?? api.config;
|
|
170
173
|
if (!isOwnerConfigured(cfg)) throw new Error("otter_not_owner");
|
|
171
174
|
const message = messageOf(params);
|
|
172
|
-
|
|
175
|
+
// The run starts outside the request that carried it (the gateway's context-free root,
|
|
176
|
+
// whatever request loaded this plugin), so it's the channel owner's, not that
|
|
177
|
+
// connection's, and outlives it.
|
|
178
|
+
runInDetachedAsyncContext(() =>
|
|
173
179
|
runDetachedWebhookWork(() => receive(api, cfg, message)).catch((error) =>
|
|
174
180
|
api.logger?.warn?.(`otter: a message from Otter failed: ${String(error)}`),
|
|
175
181
|
),
|
package/otter.js
CHANGED
|
@@ -12,6 +12,36 @@ export const OWNER_ENTRY = `otter:${OWNER}`;
|
|
|
12
12
|
/** A chat in Otter is a target of its own: what's sent to it is kept in its session. */
|
|
13
13
|
export const chatTarget = (sessionKey) => `chat:${sessionKey}`;
|
|
14
14
|
|
|
15
|
+
/** Like posting to a Slack channel rather than in a thread: each delivery starts a chat of its own. */
|
|
16
|
+
export const NEW_CHAT_TARGET = "new";
|
|
17
|
+
|
|
18
|
+
export const isNewChat = (to) =>
|
|
19
|
+
typeof to === "string" && to.trim().replace(/^otter:/, "") === NEW_CHAT_TARGET;
|
|
20
|
+
|
|
21
|
+
/** A target as the channel keeps it: one of Otter's chats, a new one each time, or the inbox. */
|
|
22
|
+
export function targetOf(to) {
|
|
23
|
+
const chat = chatOf(to);
|
|
24
|
+
if (chat) return chatTarget(chat);
|
|
25
|
+
return isNewChat(to) ? NEW_CHAT_TARGET : OTTER_TARGET;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A session for a new chat, an Otter chat like one you start. */
|
|
29
|
+
export const newChatKey = (agentId, id) => `agent:${agentId}:otter-mail:${id}`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What an agent in an Otter chat is told about sending to Otter (the message tool, an automation's
|
|
33
|
+
* delivery), with this chat's own target.
|
|
34
|
+
*/
|
|
35
|
+
export function otterTargetsNote(sessionKey) {
|
|
36
|
+
const here = agentOf(sessionKey) ? chatTarget(sessionKey) : "chat:<session key>";
|
|
37
|
+
return [
|
|
38
|
+
"Otter delivery targets (channel `otter`), like a thread or a new post in Slack:",
|
|
39
|
+
`- "${here}": this chat. Any Otter chat is "chat:<its session key>".`,
|
|
40
|
+
'- "new": a new Otter chat for each delivery (an automation that should post a fresh chat every run, such as a morning brief).',
|
|
41
|
+
'- "inbox": the agent\'s single Otter inbox chat.',
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
export const agentOf = (sessionKey) =>
|
|
16
46
|
typeof sessionKey === "string" && sessionKey.startsWith("agent:")
|
|
17
47
|
? sessionKey.split(":")[1]
|
|
@@ -41,7 +71,8 @@ export function pointerOf({ sessionKey, to, payload, at = Date.now() }) {
|
|
|
41
71
|
const approval = data.execApproval;
|
|
42
72
|
const question = data.askUser;
|
|
43
73
|
const session = approval?.sessionKey ?? sessionKey;
|
|
44
|
-
|
|
74
|
+
// A new chat's session is the one its delivery was routed to.
|
|
75
|
+
const chat = chatOf(to) ?? (isNewChat(to) && agentOf(sessionKey) ? sessionKey : undefined);
|
|
45
76
|
const agentId = approval?.agentId ?? agentOf(chat ?? session) ?? "main";
|
|
46
77
|
return {
|
|
47
78
|
kind: approval ? "approval" : question ? "question" : "message",
|
package/package.json
CHANGED