doover-js 0.6.1 → 0.6.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/dist/apis/agents-api.d.ts +8 -0
- package/dist/apis/agents-api.js +55 -2
- package/dist/react/useMultiAgentChannelMessages.d.ts +12 -0
- package/dist/react/useMultiAgentChannelMessages.js +9 -1
- package/dist/rpc/rpc-dispatcher.js +4 -2
- package/dist/test/api-overloads.test.js +3 -2
- package/dist/test/apis.test.js +76 -0
- package/dist/types/audit.d.ts +1 -1
- package/package.json +1 -1
|
@@ -32,6 +32,14 @@ export declare class AgentsApi {
|
|
|
32
32
|
private readonly controlApiUrl?;
|
|
33
33
|
constructor(rest: RestClient, controlApiUrl?: string | undefined);
|
|
34
34
|
listAgents(options?: GetAgentsOptions): Promise<AgentsResponse>;
|
|
35
|
+
/**
|
|
36
|
+
* Batch-fetch recent messages for many agents. Auto-chunks `agent_id`
|
|
37
|
+
* (and the parallel `agent_before` cursors, if given) at MULTI_AGENT_CHUNK_SIZE
|
|
38
|
+
* so each request's URL stays under CloudFront's 8,192-byte quota. Chunks
|
|
39
|
+
* are fetched in parallel and merged: `results`/`count` concat-and-sum,
|
|
40
|
+
* `next_cursors`/`at_limit_agent_ids` union, legacy `next` becomes the
|
|
41
|
+
* lexically-highest cursor across at-limit agents.
|
|
42
|
+
*/
|
|
35
43
|
getMultiAgentMessages(channelName: string, params: MultiAgentMessagesParams): Promise<BatchMessagesResponse>;
|
|
36
44
|
/**
|
|
37
45
|
* Batch-fetch aggregates for many agents. Auto-chunks `agent_id` so each
|
package/dist/apis/agents-api.js
CHANGED
|
@@ -144,12 +144,65 @@ class AgentsApi {
|
|
|
144
144
|
}
|
|
145
145
|
return { ...raw, agents: merged, results: merged, count: merged.length };
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Batch-fetch recent messages for many agents. Auto-chunks `agent_id`
|
|
149
|
+
* (and the parallel `agent_before` cursors, if given) at MULTI_AGENT_CHUNK_SIZE
|
|
150
|
+
* so each request's URL stays under CloudFront's 8,192-byte quota. Chunks
|
|
151
|
+
* are fetched in parallel and merged: `results`/`count` concat-and-sum,
|
|
152
|
+
* `next_cursors`/`at_limit_agent_ids` union, legacy `next` becomes the
|
|
153
|
+
* lexically-highest cursor across at-limit agents.
|
|
154
|
+
*/
|
|
147
155
|
async getMultiAgentMessages(channelName, params) {
|
|
148
|
-
const
|
|
149
|
-
|
|
156
|
+
const path = `/agents/channels/${channelName}/messages`;
|
|
157
|
+
const { agent_id, agent_before, ...rest } = params;
|
|
158
|
+
const stampTimestamps = (response) => ({
|
|
150
159
|
...response,
|
|
151
160
|
results: response.results.map((message) => "timestamp" in message ? message : (0, snowflake_1.addTimestampToMessage)(message)),
|
|
161
|
+
});
|
|
162
|
+
if (agent_id.length <= MULTI_AGENT_CHUNK_SIZE) {
|
|
163
|
+
const response = await this.rest.get(path, params);
|
|
164
|
+
return stampTimestamps(response);
|
|
165
|
+
}
|
|
166
|
+
if (agent_before && agent_before.length !== agent_id.length) {
|
|
167
|
+
throw new Error("agent_before must be the same length as agent_id when set");
|
|
168
|
+
}
|
|
169
|
+
const chunks = [];
|
|
170
|
+
for (let i = 0; i < agent_id.length; i += MULTI_AGENT_CHUNK_SIZE) {
|
|
171
|
+
chunks.push({
|
|
172
|
+
agent_id: agent_id.slice(i, i + MULTI_AGENT_CHUNK_SIZE),
|
|
173
|
+
...(agent_before
|
|
174
|
+
? { agent_before: agent_before.slice(i, i + MULTI_AGENT_CHUNK_SIZE) }
|
|
175
|
+
: {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const responses = await Promise.all(chunks.map((chunk) => this.rest
|
|
179
|
+
.get(path, { ...rest, ...chunk })
|
|
180
|
+
.then(stampTimestamps)));
|
|
181
|
+
// Merge: results/count concat-and-sum; next_cursors merge; at_limit_agent_ids
|
|
182
|
+
// concat (both keyed by agent id, unique across chunks). Legacy `next` is the
|
|
183
|
+
// lexically-highest oldest-cursor across at-limit agents — snowflakes sort
|
|
184
|
+
// lexically since they're equal-width digit strings.
|
|
185
|
+
const merged = {
|
|
186
|
+
results: responses.flatMap((r) => r.results),
|
|
187
|
+
count: responses.reduce((acc, r) => acc + r.count, 0),
|
|
152
188
|
};
|
|
189
|
+
const nexts = responses
|
|
190
|
+
.map((r) => r.next)
|
|
191
|
+
.filter((n) => typeof n === "string" && n.length > 0);
|
|
192
|
+
if (nexts.length > 0) {
|
|
193
|
+
merged.next = nexts.reduce((a, b) => (a > b ? a : b));
|
|
194
|
+
}
|
|
195
|
+
const cursors = {};
|
|
196
|
+
for (const r of responses) {
|
|
197
|
+
if (r.next_cursors)
|
|
198
|
+
Object.assign(cursors, r.next_cursors);
|
|
199
|
+
}
|
|
200
|
+
if (Object.keys(cursors).length > 0)
|
|
201
|
+
merged.next_cursors = cursors;
|
|
202
|
+
const atLimit = responses.flatMap((r) => r.at_limit_agent_ids ?? []);
|
|
203
|
+
if (atLimit.length > 0)
|
|
204
|
+
merged.at_limit_agent_ids = atLimit;
|
|
205
|
+
return merged;
|
|
153
206
|
}
|
|
154
207
|
/**
|
|
155
208
|
* Batch-fetch aggregates for many agents. Auto-chunks `agent_id` so each
|
|
@@ -5,7 +5,19 @@ export declare function multiAgentChannelMessagesQueryKey(channelName: string, a
|
|
|
5
5
|
fields?: readonly string[];
|
|
6
6
|
}): readonly ["doover", "channel", string, "messages", string, "src", string] | readonly ["doover", "channel", string, "messages", string, "src", string, Record<string, unknown>];
|
|
7
7
|
export interface UseMultiAgentChannelMessagesOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Global cap on the total number of messages returned across all agents
|
|
10
|
+
* for a single request. The server distributes this budget across agents
|
|
11
|
+
* in newest-first order, so a noisy agent can starve quieter ones.
|
|
12
|
+
* Prefer `agentMessageLimit` when you want a fair per-agent slice.
|
|
13
|
+
*/
|
|
8
14
|
limit?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Per-agent cap on messages returned. Forwarded as `agent_message_limit`.
|
|
17
|
+
* Use this when fanning out across many agents that have wildly different
|
|
18
|
+
* message rates and you want each agent's window represented.
|
|
19
|
+
*/
|
|
20
|
+
agentMessageLimit?: number;
|
|
9
21
|
/** If false, skip live subscriptions per agent. Defaults true. */
|
|
10
22
|
liveUpdates?: boolean;
|
|
11
23
|
/**
|
|
@@ -34,6 +34,7 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
|
|
|
34
34
|
const client = (0, context_1.useDooverClient)();
|
|
35
35
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
36
36
|
const limit = options?.limit;
|
|
37
|
+
const agentMessageLimit = options?.agentMessageLimit;
|
|
37
38
|
const liveUpdates = options?.liveUpdates ?? true;
|
|
38
39
|
const fields = options?.fields;
|
|
39
40
|
const initialBefore = options?.initialBefore;
|
|
@@ -59,8 +60,12 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
|
|
|
59
60
|
};
|
|
60
61
|
});
|
|
61
62
|
},
|
|
63
|
+
// `key` captures every scope dim (agentIds + sources + after + fields);
|
|
64
|
+
// serialise the whole thing so the closure refreshes when any dim
|
|
65
|
+
// changes — listing dims by hand drops live updates whenever the list
|
|
66
|
+
// grows (we used to omit `after`/`fields`).
|
|
62
67
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
-
[queryClient,
|
|
68
|
+
[queryClient, JSON.stringify(key)]);
|
|
64
69
|
(0, react_1.useEffect)(() => {
|
|
65
70
|
if (!liveUpdates || agentIds.length === 0)
|
|
66
71
|
return;
|
|
@@ -88,6 +93,9 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
|
|
|
88
93
|
agent_id: agentIds,
|
|
89
94
|
...(typeof pageParam === "string" ? { before: pageParam } : {}),
|
|
90
95
|
...(limit !== undefined ? { limit } : {}),
|
|
96
|
+
...(agentMessageLimit !== undefined
|
|
97
|
+
? { agent_message_limit: agentMessageLimit }
|
|
98
|
+
: {}),
|
|
91
99
|
...(fields && fields.length > 0 ? { field_name: fields } : {}),
|
|
92
100
|
...(after !== undefined ? { after } : {}),
|
|
93
101
|
};
|
|
@@ -31,8 +31,10 @@ class RpcDispatcher {
|
|
|
31
31
|
const startedAt = this.stats?.recordRpcStart() ?? null;
|
|
32
32
|
this.messages
|
|
33
33
|
.postMessage(channel.agentId, channel.channelName, {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
data: {
|
|
35
|
+
type: "rpc",
|
|
36
|
+
...request,
|
|
37
|
+
},
|
|
36
38
|
})
|
|
37
39
|
.then((message) => {
|
|
38
40
|
const pending = {
|
|
@@ -77,8 +77,9 @@ describe("MessagesApi overloads + defaults", () => {
|
|
|
77
77
|
const restB = makeRestStub();
|
|
78
78
|
const apiA = new messages_api_1.MessagesApi(restA);
|
|
79
79
|
const apiB = new messages_api_1.MessagesApi(restB);
|
|
80
|
-
|
|
81
|
-
await
|
|
80
|
+
const before = "185536526405337088";
|
|
81
|
+
await apiA.listMessages("a1", "c1", { limit: 5, order: "desc", before });
|
|
82
|
+
await apiB.listMessages({ agentId: "a1", channelName: "c1" }, { limit: 5, order: "desc", before });
|
|
82
83
|
(0, chai_1.expect)(restA.calls).to.deep.equal(restB.calls);
|
|
83
84
|
});
|
|
84
85
|
it("listMessages defaults limit to 10 when omitted", async () => {
|
package/dist/test/apis.test.js
CHANGED
|
@@ -82,6 +82,82 @@ function setupRest(responseFactory) {
|
|
|
82
82
|
const returnedIds = aggregates.results.map((r) => r.agent_id).sort();
|
|
83
83
|
(0, chai_1.expect)(returnedIds).to.deep.equal([...agentIds].sort());
|
|
84
84
|
});
|
|
85
|
+
(0, mocha_1.it)("chunks getMultiAgentMessages over the per-request agent cap", async () => {
|
|
86
|
+
const baseTs = new Date("2026-01-01T00:00:00Z");
|
|
87
|
+
const { rest, fetchMock } = setupRest((url) => {
|
|
88
|
+
const ids = [...new URL(url).searchParams.getAll("agent_id")];
|
|
89
|
+
return (0, helpers_1.createJsonResponse)({
|
|
90
|
+
results: ids.map((id, i) => ({
|
|
91
|
+
id: (0, snowflake_1.generateSnowflakeIdAtTime)(new Date(baseTs.getTime() + i)),
|
|
92
|
+
author_id: "u",
|
|
93
|
+
channel: { agent_id: id, name: "c1" },
|
|
94
|
+
data: {},
|
|
95
|
+
attachments: [],
|
|
96
|
+
})),
|
|
97
|
+
count: ids.length,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const api = new agents_api_1.AgentsApi(rest);
|
|
101
|
+
// 600 agents → 3 chunks of 250/250/100.
|
|
102
|
+
const agentIds = Array.from({ length: 600 }, (_, i) => `a${i}`);
|
|
103
|
+
const messages = await api.getMultiAgentMessages("c1", {
|
|
104
|
+
agent_id: agentIds,
|
|
105
|
+
agent_message_limit: 50,
|
|
106
|
+
});
|
|
107
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(3);
|
|
108
|
+
(0, chai_1.expect)(messages.results).to.have.length(600);
|
|
109
|
+
(0, chai_1.expect)(messages.count).to.equal(600);
|
|
110
|
+
const returnedIds = messages.results
|
|
111
|
+
.map((m) => m.channel.agent_id)
|
|
112
|
+
.sort();
|
|
113
|
+
(0, chai_1.expect)(returnedIds).to.deep.equal([...agentIds].sort());
|
|
114
|
+
// Per-request param survives chunking.
|
|
115
|
+
for (let i = 0; i < 3; i++) {
|
|
116
|
+
const url = fetchMock.getCall(i).args[0];
|
|
117
|
+
(0, chai_1.expect)(url).to.include("agent_message_limit=50");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
(0, mocha_1.it)("merges next_cursors and at_limit_agent_ids across chunks", async () => {
|
|
121
|
+
let call = 0;
|
|
122
|
+
const cursors = [
|
|
123
|
+
{ "a0": "100", "a1": "200" },
|
|
124
|
+
{ "a250": "150" },
|
|
125
|
+
];
|
|
126
|
+
const atLimit = [["a0", "a1"], ["a250"]];
|
|
127
|
+
const { rest } = setupRest((url) => {
|
|
128
|
+
const idx = call++;
|
|
129
|
+
const ids = [...new URL(url).searchParams.getAll("agent_id")];
|
|
130
|
+
return (0, helpers_1.createJsonResponse)({
|
|
131
|
+
results: ids.map((id) => ({
|
|
132
|
+
id: (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00Z")),
|
|
133
|
+
author_id: "u",
|
|
134
|
+
channel: { agent_id: id, name: "c1" },
|
|
135
|
+
data: {},
|
|
136
|
+
attachments: [],
|
|
137
|
+
})),
|
|
138
|
+
count: ids.length,
|
|
139
|
+
next: idx === 0 ? "200" : "150",
|
|
140
|
+
next_cursors: cursors[idx],
|
|
141
|
+
at_limit_agent_ids: atLimit[idx],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
const api = new agents_api_1.AgentsApi(rest);
|
|
145
|
+
const agentIds = Array.from({ length: 300 }, (_, i) => `a${i}`);
|
|
146
|
+
const messages = await api.getMultiAgentMessages("c1", {
|
|
147
|
+
agent_id: agentIds,
|
|
148
|
+
});
|
|
149
|
+
(0, chai_1.expect)(messages.next).to.equal("200"); // max across chunks
|
|
150
|
+
(0, chai_1.expect)(messages.next_cursors).to.deep.equal({
|
|
151
|
+
a0: "100",
|
|
152
|
+
a1: "200",
|
|
153
|
+
a250: "150",
|
|
154
|
+
});
|
|
155
|
+
(0, chai_1.expect)(messages.at_limit_agent_ids?.sort()).to.deep.equal([
|
|
156
|
+
"a0",
|
|
157
|
+
"a1",
|
|
158
|
+
"a250",
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
85
161
|
(0, mocha_1.it)("covers channels methods", async () => {
|
|
86
162
|
const { rest, fetchMock } = setupRest(() => (0, helpers_1.createJsonResponse)([]));
|
|
87
163
|
const api = new channels_api_1.ChannelsApi(rest);
|
package/dist/types/audit.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface AuditRequestContext {
|
|
|
27
27
|
user_agent?: string;
|
|
28
28
|
}
|
|
29
29
|
/** Origin system for the event. */
|
|
30
|
-
export type AuditSource = "control" | "
|
|
30
|
+
export type AuditSource = "control" | "auth" | "data" | "tunnels";
|
|
31
31
|
/**
|
|
32
32
|
* String-literal union of every wired audit action — kept in lock-step with
|
|
33
33
|
* ``doover_control/events/types.py``'s ``EventType`` enum. Future event types
|