agentfeed 0.1.8 → 0.1.11
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/agent-registry-store.d.ts +11 -0
- package/dist/agent-registry-store.js +31 -0
- package/dist/api-client.d.ts +7 -4
- package/dist/api-client.js +25 -10
- package/dist/backends/claude.d.ts +12 -0
- package/dist/backends/claude.js +95 -0
- package/dist/backends/codex.d.ts +13 -0
- package/dist/backends/codex.js +72 -0
- package/dist/backends/gemini.d.ts +11 -0
- package/dist/backends/gemini.js +105 -0
- package/dist/backends/index.d.ts +6 -0
- package/dist/backends/index.js +16 -0
- package/dist/backends/types.d.ts +24 -0
- package/dist/backends/types.js +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +129 -0
- package/dist/follow-store.d.ts +1 -0
- package/dist/follow-store.js +12 -0
- package/dist/index.js +142 -200
- package/dist/invoker.d.ts +6 -4
- package/dist/invoker.js +75 -75
- package/dist/mcp-server.js +37 -0
- package/dist/post-session-store.d.ts +11 -1
- package/dist/post-session-store.js +41 -4
- package/dist/processor.d.ts +21 -0
- package/dist/processor.js +170 -0
- package/dist/queue-store.js +2 -2
- package/dist/scanner.d.ts +2 -2
- package/dist/scanner.js +46 -28
- package/dist/session-store.d.ts +1 -0
- package/dist/session-store.js +3 -0
- package/dist/sse-client.d.ts +1 -1
- package/dist/sse-client.js +7 -3
- package/dist/trigger.d.ts +2 -2
- package/dist/trigger.js +79 -52
- package/dist/types.d.ts +17 -0
- package/package.json +14 -1
- package/dist/mcp-config.d.ts +0 -11
- package/dist/mcp-config.js +0 -25
package/dist/mcp-server.js
CHANGED
|
@@ -71,6 +71,17 @@ const TOOLS = [
|
|
|
71
71
|
required: ["post_id", "content"],
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: "agentfeed_download_file",
|
|
76
|
+
description: "Download a file from AgentFeed uploads. For images, returns the image so you can see it. Use this when content contains image URLs like ",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
url: { type: "string", description: "File URL (e.g. /api/uploads/up_xxx.png or full URL)" },
|
|
81
|
+
},
|
|
82
|
+
required: ["url"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
74
85
|
{
|
|
75
86
|
name: "agentfeed_set_status",
|
|
76
87
|
description: "Report agent status (thinking/idle)",
|
|
@@ -147,6 +158,32 @@ async function handleToolCall(name, args, ctx) {
|
|
|
147
158
|
const comment = await res.json();
|
|
148
159
|
return { content: [{ type: "text", text: `Comment posted: ${comment.id}` }] };
|
|
149
160
|
}
|
|
161
|
+
case "agentfeed_download_file": {
|
|
162
|
+
const { url } = args;
|
|
163
|
+
// Resolve relative URLs to absolute
|
|
164
|
+
const fullUrl = url.startsWith("/") ? `${serverUrl}${url}` : url;
|
|
165
|
+
const fileRes = await fetch(fullUrl);
|
|
166
|
+
if (!fileRes.ok)
|
|
167
|
+
throw new Error(`Failed to download file: ${fileRes.status}`);
|
|
168
|
+
const contentType = fileRes.headers.get("content-type") || "application/octet-stream";
|
|
169
|
+
const isImage = contentType.startsWith("image/");
|
|
170
|
+
if (isImage) {
|
|
171
|
+
const buffer = await fileRes.arrayBuffer();
|
|
172
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{ type: "image", data: base64, mimeType: contentType },
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Non-image files: return metadata
|
|
180
|
+
const size = fileRes.headers.get("content-length") || "unknown";
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{ type: "text", text: `File downloaded: ${url}\nType: ${contentType}\nSize: ${size} bytes\n(Non-image files cannot be displayed inline)` },
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
150
187
|
case "agentfeed_set_status": {
|
|
151
188
|
const { status, feed_id, post_id } = args;
|
|
152
189
|
await client.setAgentStatus({ status, feed_id, post_id });
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { PersistentStore } from "./persistent-store.js";
|
|
2
|
+
import type { BackendType } from "./types.js";
|
|
3
|
+
export interface PostSessionInfo {
|
|
4
|
+
backendType: BackendType;
|
|
5
|
+
sessionName: string;
|
|
6
|
+
}
|
|
2
7
|
export declare class PostSessionStore extends PersistentStore {
|
|
3
8
|
private map;
|
|
4
9
|
constructor(filePath?: string);
|
|
5
10
|
protected serialize(): string;
|
|
6
11
|
protected deserialize(raw: string): void;
|
|
12
|
+
/** Get raw sessionName (legacy compat — for callers that don't need backendType) */
|
|
7
13
|
get(postId: string): string | undefined;
|
|
8
|
-
|
|
14
|
+
/** Get both backendType and sessionName. Legacy values without colon default to claude. */
|
|
15
|
+
getWithType(postId: string): PostSessionInfo | undefined;
|
|
16
|
+
private static readonly MAX_SIZE;
|
|
17
|
+
/** Store as "backendType:sessionName" */
|
|
18
|
+
set(postId: string, backendType: BackendType, sessionName: string): void;
|
|
9
19
|
removeBySessionName(sessionName: string): void;
|
|
10
20
|
}
|
|
@@ -14,16 +14,53 @@ export class PostSessionStore extends PersistentStore {
|
|
|
14
14
|
this.map.set(k, v);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/** Get raw sessionName (legacy compat — for callers that don't need backendType) */
|
|
17
18
|
get(postId) {
|
|
18
|
-
|
|
19
|
+
const raw = this.map.get(postId);
|
|
20
|
+
if (!raw)
|
|
21
|
+
return undefined;
|
|
22
|
+
// Parse "backendType:sessionName" or plain "sessionName"
|
|
23
|
+
const colonIdx = raw.indexOf(":");
|
|
24
|
+
return colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
|
|
19
25
|
}
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
/** Get both backendType and sessionName. Legacy values without colon default to claude. */
|
|
27
|
+
getWithType(postId) {
|
|
28
|
+
const raw = this.map.get(postId);
|
|
29
|
+
if (!raw)
|
|
30
|
+
return undefined;
|
|
31
|
+
const colonIdx = raw.indexOf(":");
|
|
32
|
+
if (colonIdx >= 0) {
|
|
33
|
+
return {
|
|
34
|
+
backendType: raw.slice(0, colonIdx),
|
|
35
|
+
sessionName: raw.slice(colonIdx + 1),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Legacy: no colon → assume claude
|
|
39
|
+
return { backendType: "claude", sessionName: raw };
|
|
40
|
+
}
|
|
41
|
+
static MAX_SIZE = 1000;
|
|
42
|
+
/** Store as "backendType:sessionName" */
|
|
43
|
+
set(postId, backendType, sessionName) {
|
|
44
|
+
this.map.set(postId, `${backendType}:${sessionName}`);
|
|
45
|
+
// Evict oldest entries if over limit
|
|
46
|
+
if (this.map.size > PostSessionStore.MAX_SIZE) {
|
|
47
|
+
const iter = this.map.keys();
|
|
48
|
+
while (this.map.size > PostSessionStore.MAX_SIZE) {
|
|
49
|
+
const oldest = iter.next().value;
|
|
50
|
+
if (oldest !== undefined)
|
|
51
|
+
this.map.delete(oldest);
|
|
52
|
+
else
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
22
56
|
this.save();
|
|
23
57
|
}
|
|
24
58
|
removeBySessionName(sessionName) {
|
|
25
59
|
let changed = false;
|
|
26
|
-
for (const [postId,
|
|
60
|
+
for (const [postId, raw] of this.map) {
|
|
61
|
+
// Match both "backendType:sessionName" and legacy "sessionName"
|
|
62
|
+
const colonIdx = raw.indexOf(":");
|
|
63
|
+
const name = colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
|
|
27
64
|
if (name === sessionName) {
|
|
28
65
|
this.map.delete(postId);
|
|
29
66
|
changed = true;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AgentFeedClient } from "./api-client.js";
|
|
2
|
+
import { FollowStore } from "./follow-store.js";
|
|
3
|
+
import { QueueStore } from "./queue-store.js";
|
|
4
|
+
import { PostSessionStore } from "./post-session-store.js";
|
|
5
|
+
import { AgentRegistryStore } from "./agent-registry-store.js";
|
|
6
|
+
import type { TriggerContext, PermissionMode, BackendType, BackendAgent } from "./types.js";
|
|
7
|
+
export interface ProcessorDeps {
|
|
8
|
+
client: AgentFeedClient;
|
|
9
|
+
apiKey: string;
|
|
10
|
+
serverUrl: string;
|
|
11
|
+
permissionMode: PermissionMode;
|
|
12
|
+
extraAllowedTools: string[];
|
|
13
|
+
backendAgentMap: Map<BackendType, BackendAgent>;
|
|
14
|
+
backendAgents: BackendAgent[];
|
|
15
|
+
followStore: FollowStore;
|
|
16
|
+
queueStore: QueueStore;
|
|
17
|
+
postSessionStore: PostSessionStore;
|
|
18
|
+
agentRegistry: AgentRegistryStore;
|
|
19
|
+
ensureSessionAgent: (sessionName: string, agentName: string, backendType: BackendType) => Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare function handleTriggers(triggers: TriggerContext[], deps: ProcessorDeps): void;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { invokeAgent } from "./invoker.js";
|
|
2
|
+
import { scanUnprocessed } from "./scanner.js";
|
|
3
|
+
const MAX_WAKE_ATTEMPTS = 3;
|
|
4
|
+
const MAX_CRASH_RETRIES = 3;
|
|
5
|
+
const AGENT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
const MAX_CONCURRENT = 5;
|
|
7
|
+
const MAX_BOT_MENTIONS_PER_POST = 4;
|
|
8
|
+
const wakeAttempts = new Map();
|
|
9
|
+
const botMentionCounts = new Map();
|
|
10
|
+
const runningKeys = new Set();
|
|
11
|
+
// Periodic cleanup to prevent memory growth
|
|
12
|
+
setInterval(() => { wakeAttempts.clear(); botMentionCounts.clear(); }, 10 * 60 * 1000);
|
|
13
|
+
function triggerKey(t) {
|
|
14
|
+
return `${t.backendType}:${t.sessionName}`;
|
|
15
|
+
}
|
|
16
|
+
export function handleTriggers(triggers, deps) {
|
|
17
|
+
for (const t of triggers) {
|
|
18
|
+
// Prevent bot-to-bot mention loops
|
|
19
|
+
if (t.authorIsBot && t.triggerType === "mention") {
|
|
20
|
+
const count = botMentionCounts.get(t.postId) ?? 0;
|
|
21
|
+
if (count >= MAX_BOT_MENTIONS_PER_POST) {
|
|
22
|
+
console.log(`Skipping bot mention on ${t.postId}: loop limit (${MAX_BOT_MENTIONS_PER_POST}) reached`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
botMentionCounts.set(t.postId, count + 1);
|
|
26
|
+
}
|
|
27
|
+
deps.queueStore.push(t);
|
|
28
|
+
console.log(`Queued trigger: ${t.triggerType} on ${t.postId} [${t.backendType}] (queue size: ${deps.queueStore.size})`);
|
|
29
|
+
}
|
|
30
|
+
scheduleQueue(deps);
|
|
31
|
+
}
|
|
32
|
+
function scheduleQueue(deps) {
|
|
33
|
+
const queued = deps.queueStore.drain();
|
|
34
|
+
if (queued.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const toRun = [];
|
|
37
|
+
for (const t of queued) {
|
|
38
|
+
const attempts = wakeAttempts.get(t.eventId) ?? 0;
|
|
39
|
+
if (attempts >= MAX_WAKE_ATTEMPTS) {
|
|
40
|
+
console.log(`Skipping ${t.eventId}: max wake attempts reached`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const key = triggerKey(t);
|
|
44
|
+
if (runningKeys.size >= MAX_CONCURRENT || runningKeys.has(key)) {
|
|
45
|
+
// Same backend+session already running — re-queue
|
|
46
|
+
deps.queueStore.push(t);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
toRun.push(t);
|
|
50
|
+
runningKeys.add(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const trigger of toRun) {
|
|
54
|
+
processItem(trigger, deps).catch((err) => {
|
|
55
|
+
console.error(`Error processing ${trigger.postId}:`, err);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function processItem(trigger, deps) {
|
|
60
|
+
const key = triggerKey(trigger);
|
|
61
|
+
wakeAttempts.set(trigger.eventId, (wakeAttempts.get(trigger.eventId) ?? 0) + 1);
|
|
62
|
+
const ba = deps.backendAgentMap.get(trigger.backendType);
|
|
63
|
+
if (!ba) {
|
|
64
|
+
console.warn(`No backend for ${trigger.backendType}, skipping`);
|
|
65
|
+
runningKeys.delete(key);
|
|
66
|
+
scheduleQueue(deps);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
// Auto-follow thread on mention
|
|
71
|
+
if (trigger.triggerType === "mention") {
|
|
72
|
+
deps.followStore.add(trigger.postId);
|
|
73
|
+
console.log(`Following thread: ${trigger.postId}`);
|
|
74
|
+
}
|
|
75
|
+
const sessionAgentId = await deps.ensureSessionAgent(trigger.sessionName, ba.agent.name, ba.backendType);
|
|
76
|
+
const recentContext = await fetchContext(trigger, deps);
|
|
77
|
+
console.log(`Waking agent for: ${trigger.triggerType} on ${trigger.postId} [${trigger.backendType}] (session: ${trigger.sessionName}, agentId: ${sessionAgentId})`);
|
|
78
|
+
await deps.client.setAgentStatus({
|
|
79
|
+
status: "thinking",
|
|
80
|
+
feed_id: trigger.feedId,
|
|
81
|
+
post_id: trigger.postId,
|
|
82
|
+
}, sessionAgentId);
|
|
83
|
+
// Server config overrides CLI args
|
|
84
|
+
const effectivePermission = ba.config?.permission_mode ?? deps.permissionMode;
|
|
85
|
+
const effectiveTools = ba.config?.allowed_tools?.length
|
|
86
|
+
? ba.config.allowed_tools
|
|
87
|
+
: deps.extraAllowedTools;
|
|
88
|
+
const effectiveModel = ba.config?.model;
|
|
89
|
+
let retries = 0;
|
|
90
|
+
let success = false;
|
|
91
|
+
try {
|
|
92
|
+
while (retries < MAX_CRASH_RETRIES) {
|
|
93
|
+
try {
|
|
94
|
+
const result = await invokeAgent(ba.backend, {
|
|
95
|
+
agent: ba.agent,
|
|
96
|
+
trigger,
|
|
97
|
+
apiKey: deps.apiKey,
|
|
98
|
+
serverUrl: deps.serverUrl,
|
|
99
|
+
recentContext,
|
|
100
|
+
permissionMode: effectivePermission,
|
|
101
|
+
extraAllowedTools: effectiveTools,
|
|
102
|
+
model: effectiveModel ?? undefined,
|
|
103
|
+
sessionId: ba.sessionStore.get(trigger.sessionName),
|
|
104
|
+
agentId: sessionAgentId,
|
|
105
|
+
timeoutMs: AGENT_TIMEOUT_MS,
|
|
106
|
+
});
|
|
107
|
+
if (result.sessionId) {
|
|
108
|
+
ba.sessionStore.set(trigger.sessionName, result.sessionId);
|
|
109
|
+
deps.postSessionStore.set(trigger.postId, trigger.backendType, trigger.sessionName);
|
|
110
|
+
await deps.client.reportSession(trigger.sessionName, result.sessionId, sessionAgentId).catch((err) => {
|
|
111
|
+
console.warn("Failed to report session:", err);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (result.exitCode === 0) {
|
|
115
|
+
success = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (result.exitCode !== 0 && ba.sessionStore.get(trigger.sessionName)) {
|
|
119
|
+
console.log("Session may be stale, clearing and retrying as new session...");
|
|
120
|
+
ba.sessionStore.delete(trigger.sessionName);
|
|
121
|
+
}
|
|
122
|
+
console.error(`Agent exited with code ${result.exitCode}, retry ${retries + 1}/${MAX_CRASH_RETRIES}`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.error("Agent invocation error:", err);
|
|
126
|
+
}
|
|
127
|
+
retries++;
|
|
128
|
+
}
|
|
129
|
+
if (!success) {
|
|
130
|
+
console.error(`Agent failed after ${MAX_CRASH_RETRIES} retries`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await deps.client.setAgentStatus({
|
|
135
|
+
status: "idle",
|
|
136
|
+
feed_id: trigger.feedId,
|
|
137
|
+
post_id: trigger.postId,
|
|
138
|
+
}, sessionAgentId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
runningKeys.delete(key);
|
|
143
|
+
// Post-completion: re-scan for items that arrived during execution
|
|
144
|
+
try {
|
|
145
|
+
const currentOwnIds = deps.agentRegistry.getAllIds();
|
|
146
|
+
const newUnprocessed = await scanUnprocessed(deps.client, deps.backendAgents, deps.followStore, deps.postSessionStore, currentOwnIds);
|
|
147
|
+
for (const t of newUnprocessed) {
|
|
148
|
+
deps.queueStore.push(t);
|
|
149
|
+
}
|
|
150
|
+
if (newUnprocessed.length > 0) {
|
|
151
|
+
console.log(`Post-completion scan: ${newUnprocessed.length} item(s) added to queue`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
console.error("Post-completion scan error:", err);
|
|
156
|
+
}
|
|
157
|
+
scheduleQueue(deps);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function fetchContext(trigger, deps) {
|
|
161
|
+
try {
|
|
162
|
+
const comments = await deps.client.getPostComments(trigger.postId, { limit: 10 });
|
|
163
|
+
return comments.data
|
|
164
|
+
.map((c) => `[${c.author_type}${c.author_name ? ` (${c.author_name})` : ""}] ${c.content}`)
|
|
165
|
+
.join("\n");
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/queue-store.js
CHANGED
|
@@ -15,8 +15,8 @@ export class QueueStore extends PersistentStore {
|
|
|
15
15
|
// Deduplicate by eventId
|
|
16
16
|
if (this.queue.some((t) => t.eventId === trigger.eventId))
|
|
17
17
|
return;
|
|
18
|
-
// Deduplicate by postId — keep only the latest trigger per post
|
|
19
|
-
this.queue = this.queue.filter((t) => t.postId
|
|
18
|
+
// Deduplicate by (postId, backendType) — keep only the latest trigger per post per backend
|
|
19
|
+
this.queue = this.queue.filter((t) => !(t.postId === trigger.postId && t.backendType === trigger.backendType));
|
|
20
20
|
this.queue.push(trigger);
|
|
21
21
|
this.save();
|
|
22
22
|
}
|
package/dist/scanner.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TriggerContext, BackendAgent } from "./types.js";
|
|
2
2
|
import type { AgentFeedClient } from "./api-client.js";
|
|
3
3
|
import type { FollowStore } from "./follow-store.js";
|
|
4
4
|
import type { PostSessionStore } from "./post-session-store.js";
|
|
5
|
-
export declare function scanUnprocessed(client: AgentFeedClient,
|
|
5
|
+
export declare function scanUnprocessed(client: AgentFeedClient, backendAgents: BackendAgent[], followStore?: FollowStore, postSessionStore?: PostSessionStore, ownAgentIds?: Set<string>): Promise<TriggerContext[]>;
|
package/dist/scanner.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { parseMention, isBotAuthor } from "./utils.js";
|
|
2
|
-
export async function scanUnprocessed(client,
|
|
2
|
+
export async function scanUnprocessed(client, backendAgents, followStore, postSessionStore, ownAgentIds) {
|
|
3
3
|
const triggers = [];
|
|
4
4
|
const feeds = await client.getFeeds();
|
|
5
5
|
const processedPostIds = new Set();
|
|
6
|
+
const isOwnAgent = (id) => id !== null && (ownAgentIds ? ownAgentIds.has(id) : false);
|
|
7
|
+
const defaultBackendType = backendAgents[0]?.backendType ?? "claude";
|
|
6
8
|
for (const feed of feeds) {
|
|
7
9
|
// --- Scan comments (existing logic) ---
|
|
8
10
|
const comments = await client.getFeedComments(feed.id, {
|
|
@@ -21,27 +23,35 @@ export async function scanUnprocessed(client, agent, followStore, postSessionSto
|
|
|
21
23
|
let bestTriggerType = null;
|
|
22
24
|
let bestComment = null;
|
|
23
25
|
let bestSessionName = "default";
|
|
26
|
+
let bestBackendType = defaultBackendType;
|
|
24
27
|
for (const comment of postComments) {
|
|
25
|
-
// Check for @mentions (highest priority)
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// Check for @mentions across all backend agents (highest priority)
|
|
29
|
+
for (const ba of backendAgents) {
|
|
30
|
+
const mention = parseMention(comment.content, ba.agent.name);
|
|
31
|
+
if (mention.mentioned) {
|
|
32
|
+
bestTriggerType = "mention";
|
|
33
|
+
bestComment = comment;
|
|
34
|
+
bestSessionName = mention.sessionName;
|
|
35
|
+
bestBackendType = ba.backendType;
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
// Check if this is on an agent-owned post
|
|
33
|
-
if (!bestTriggerType && comment.post_created_by
|
|
39
|
+
if (!bestTriggerType && isOwnAgent(comment.post_created_by)) {
|
|
40
|
+
const postSession = postSessionStore?.getWithType(postId);
|
|
34
41
|
bestTriggerType = "own_post_comment";
|
|
35
42
|
bestComment = comment;
|
|
36
|
-
bestSessionName =
|
|
43
|
+
bestSessionName = postSession?.sessionName ?? "default";
|
|
44
|
+
bestBackendType = postSession?.backendType ?? defaultBackendType;
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
// Check if this is in a followed thread
|
|
40
48
|
if (!bestTriggerType && followStore?.has(postId)) {
|
|
49
|
+
const postSession = postSessionStore?.getWithType(postId);
|
|
41
50
|
bestTriggerType = "thread_follow_up";
|
|
42
51
|
// Use the last human comment as the trigger
|
|
43
52
|
bestComment = postComments[postComments.length - 1] ?? null;
|
|
44
|
-
bestSessionName =
|
|
53
|
+
bestSessionName = postSession?.sessionName ?? "default";
|
|
54
|
+
bestBackendType = postSession?.backendType ?? defaultBackendType;
|
|
45
55
|
}
|
|
46
56
|
if (!bestTriggerType || !bestComment)
|
|
47
57
|
continue;
|
|
@@ -61,7 +71,9 @@ export async function scanUnprocessed(client, agent, followStore, postSessionSto
|
|
|
61
71
|
postId,
|
|
62
72
|
content: bestComment.content,
|
|
63
73
|
authorName: bestComment.author_name,
|
|
74
|
+
authorIsBot: false,
|
|
64
75
|
sessionName: bestSessionName,
|
|
76
|
+
backendType: bestBackendType,
|
|
65
77
|
});
|
|
66
78
|
processedPostIds.add(postId);
|
|
67
79
|
}
|
|
@@ -78,25 +90,31 @@ export async function scanUnprocessed(client, agent, followStore, postSessionSto
|
|
|
78
90
|
// Skip posts without content
|
|
79
91
|
if (!post.content)
|
|
80
92
|
continue;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
triggers.push({
|
|
91
|
-
triggerType: "mention",
|
|
92
|
-
eventId: post.id,
|
|
93
|
-
feedId: feed.id,
|
|
94
|
-
feedName: feed.name,
|
|
95
|
-
postId: post.id,
|
|
96
|
-
content: post.content,
|
|
97
|
-
authorName: post.author_name,
|
|
98
|
-
sessionName: mention.sessionName,
|
|
93
|
+
// Try mentions across all backend agents
|
|
94
|
+
for (const ba of backendAgents) {
|
|
95
|
+
const mention = parseMention(post.content, ba.agent.name);
|
|
96
|
+
if (!mention.mentioned)
|
|
97
|
+
continue;
|
|
98
|
+
// Check if there's any bot reply on this post
|
|
99
|
+
const botReplies = await client.getPostComments(post.id, {
|
|
100
|
+
author_type: "bot",
|
|
101
|
+
limit: 1,
|
|
99
102
|
});
|
|
103
|
+
if (botReplies.data.length === 0) {
|
|
104
|
+
triggers.push({
|
|
105
|
+
triggerType: "mention",
|
|
106
|
+
eventId: post.id,
|
|
107
|
+
feedId: feed.id,
|
|
108
|
+
feedName: feed.name,
|
|
109
|
+
postId: post.id,
|
|
110
|
+
content: post.content,
|
|
111
|
+
authorName: post.author_name,
|
|
112
|
+
authorIsBot: false,
|
|
113
|
+
sessionName: mention.sessionName,
|
|
114
|
+
backendType: ba.backendType,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
break; // First matching backend wins for this post
|
|
100
118
|
}
|
|
101
119
|
}
|
|
102
120
|
}
|
package/dist/session-store.d.ts
CHANGED
package/dist/session-store.js
CHANGED
package/dist/sse-client.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export interface SSEEvent {
|
|
|
6
6
|
export interface SSEConnection {
|
|
7
7
|
close: () => void;
|
|
8
8
|
}
|
|
9
|
-
export declare function connectSSE(url: string, apiKey: string,
|
|
9
|
+
export declare function connectSSE(url: string, apiKey: string, agentIds: string[], onEvent: (event: SSEEvent) => void, onError: (error: Error) => void): SSEConnection;
|
package/dist/sse-client.js
CHANGED
|
@@ -3,7 +3,7 @@ const EVENT_TYPES = ["heartbeat", "post_created", "comment_created", "session_de
|
|
|
3
3
|
const BACKOFF_INITIAL_MS = 1_000;
|
|
4
4
|
const BACKOFF_MAX_MS = 60_000;
|
|
5
5
|
const BACKOFF_RESET_AFTER_MS = 30_000;
|
|
6
|
-
export function connectSSE(url, apiKey,
|
|
6
|
+
export function connectSSE(url, apiKey, agentIds, onEvent, onError) {
|
|
7
7
|
let closed = false;
|
|
8
8
|
let currentEs = null;
|
|
9
9
|
let backoffMs = BACKOFF_INITIAL_MS;
|
|
@@ -14,13 +14,17 @@ export function connectSSE(url, apiKey, agentId, onEvent, onError) {
|
|
|
14
14
|
function connect() {
|
|
15
15
|
if (closed)
|
|
16
16
|
return;
|
|
17
|
-
|
|
17
|
+
// Pass all agent IDs as query parameter for multi-agent online tracking
|
|
18
|
+
const connectUrl = new URL(url);
|
|
19
|
+
if (agentIds.length > 0) {
|
|
20
|
+
connectUrl.searchParams.set("agents", agentIds.join(","));
|
|
21
|
+
}
|
|
22
|
+
const es = new EventSource(connectUrl.toString(), {
|
|
18
23
|
fetch: (input, init) => fetch(input, {
|
|
19
24
|
...init,
|
|
20
25
|
headers: {
|
|
21
26
|
...init.headers,
|
|
22
27
|
Authorization: `Bearer ${apiKey}`,
|
|
23
|
-
...(agentId ? { "X-Agent-Id": agentId } : {}),
|
|
24
28
|
},
|
|
25
29
|
}),
|
|
26
30
|
});
|
package/dist/trigger.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { FollowStore } from "./follow-store.js";
|
|
2
2
|
import type { PostSessionStore } from "./post-session-store.js";
|
|
3
|
-
import type { GlobalEvent, TriggerContext,
|
|
4
|
-
export declare function
|
|
3
|
+
import type { GlobalEvent, TriggerContext, BackendAgent } from "./types.js";
|
|
4
|
+
export declare function detectTriggers(event: GlobalEvent, backendAgents: BackendAgent[], followStore?: FollowStore, postSessionStore?: PostSessionStore, ownAgentIds?: Set<string>): TriggerContext[];
|