@promptbook/cli 0.112.0-109 → 0.112.0-110
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/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts +86 -4
- package/apps/agents-server/src/database/loadAgentsServerEnvFile.ts +29 -0
- package/apps/agents-server/src/database/migrate.ts +2 -25
- package/apps/agents-server/src/database/seedDefaultAgents.ts +2 -26
- package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +56 -0
- package/apps/agents-server/src/utils/importAgent.ts +57 -1
- package/apps/agents-server/src/utils/importAgentWithFallback.ts +10 -0
- package/apps/agents-server/src/utils/userChat/getUserChatRevision.ts +145 -0
- package/apps/agents-server/src/utils/userChat.ts +1 -0
- package/esm/index.es.js +226 -105
- package/esm/index.es.js.map +1 -1
- package/esm/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +17 -1
- package/esm/src/cli/cli-commands/agents-server/run.d.ts +6 -0
- package/esm/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +7 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/Chat/Chat.module.css +5 -0
- package/src/cli/cli-commands/agents-server/buildAgentsServer.ts +48 -14
- package/src/cli/cli-commands/agents-server/run.ts +103 -31
- package/src/cli/cli-commands/agents-server/startAgentsServer.ts +111 -35
- package/src/cli/cli-commands/agents-server.ts +7 -1
- package/src/other/templates/getTemplatesPipelineCollection.ts +1081 -516
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +226 -105
- package/umd/index.umd.js.map +1 -1
- package/umd/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +17 -1
- package/umd/src/cli/cli-commands/agents-server/run.d.ts +6 -0
- package/umd/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +7 -0
- package/umd/src/version.d.ts +1 -1
package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { CHAT_STREAM_KEEP_ALIVE_INTERVAL_MS } from '@/src/constants/streaming';
|
|
2
2
|
import { isPrivateModeEnabledFromRequest } from '@/src/utils/privateMode';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createUserChatDetailPayload,
|
|
5
|
+
getUserChat,
|
|
6
|
+
getUserChatRevision,
|
|
7
|
+
isFrozenUserChatSource,
|
|
8
|
+
} from '@/src/utils/userChat';
|
|
4
9
|
import type { ChatMessage } from '@promptbook-local/types';
|
|
5
10
|
import { NextResponse } from 'next/server';
|
|
6
11
|
import { resolveUserChatScope } from '../../resolveUserChatScope';
|
|
@@ -32,6 +37,11 @@ type UserChatStreamFrame =
|
|
|
32
37
|
type: 'keepalive';
|
|
33
38
|
};
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Lightweight revision shape returned by `getUserChatRevision`.
|
|
42
|
+
*/
|
|
43
|
+
type UserChatRevision = Awaited<ReturnType<typeof getUserChatRevision>>;
|
|
44
|
+
|
|
35
45
|
/**
|
|
36
46
|
* Streams canonical chat snapshots for one scoped user chat so multiple viewers can observe the same background turn.
|
|
37
47
|
*/
|
|
@@ -73,6 +83,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
73
83
|
async start(controller) {
|
|
74
84
|
let isStreamClosed = false;
|
|
75
85
|
let lastSnapshotSignature: string | null = null;
|
|
86
|
+
let lastRevisionSignature: string | null = null;
|
|
76
87
|
let keepAliveInterval: ReturnType<typeof setInterval> | null = null;
|
|
77
88
|
|
|
78
89
|
/**
|
|
@@ -138,6 +149,24 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
138
149
|
|
|
139
150
|
const payload = await createUserChatDetailPayload(currentChat);
|
|
140
151
|
const nextSignature = createUserChatDetailSignature(payload);
|
|
152
|
+
lastRevisionSignature = createUserChatRevisionSignature({
|
|
153
|
+
id: payload.chat.id,
|
|
154
|
+
updatedAt: payload.chat.updatedAt,
|
|
155
|
+
draftMessage: payload.draftMessage,
|
|
156
|
+
source: payload.chat.source,
|
|
157
|
+
activeJobs: payload.activeJobs.map((job) => ({
|
|
158
|
+
id: job.id,
|
|
159
|
+
status: job.status,
|
|
160
|
+
cancelRequestedAt: job.cancelRequestedAt,
|
|
161
|
+
})),
|
|
162
|
+
activeTimeouts: payload.activeTimeouts.map((timeout) => ({
|
|
163
|
+
id: timeout.id,
|
|
164
|
+
status: timeout.status,
|
|
165
|
+
message: timeout.message,
|
|
166
|
+
dueAt: timeout.dueAt,
|
|
167
|
+
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
168
|
+
})),
|
|
169
|
+
});
|
|
141
170
|
|
|
142
171
|
if (nextSignature !== lastSnapshotSignature) {
|
|
143
172
|
lastSnapshotSignature = nextSignature;
|
|
@@ -149,6 +178,32 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
149
178
|
return !isFrozenUserChatSource(payload.chat.source) && payload.activeJobs.length > 0;
|
|
150
179
|
};
|
|
151
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Emits a full snapshot only when an idle chat revision changed.
|
|
183
|
+
*
|
|
184
|
+
* @private route helper
|
|
185
|
+
*/
|
|
186
|
+
const emitLatestSnapshotWhenRevisionChanged = async (): Promise<boolean> => {
|
|
187
|
+
const currentRevision = await getUserChatRevision({
|
|
188
|
+
userId: scopeResult.scope.userId,
|
|
189
|
+
viewerIsAdmin: scopeResult.scope.viewerIsAdmin,
|
|
190
|
+
agentPermanentId: scopeResult.scope.agentPermanentId,
|
|
191
|
+
chatId,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!currentRevision) {
|
|
195
|
+
closeStream();
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const nextRevisionSignature = createUserChatRevisionSignature(currentRevision);
|
|
200
|
+
if (nextRevisionSignature === lastRevisionSignature) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return emitLatestSnapshot();
|
|
205
|
+
};
|
|
206
|
+
|
|
152
207
|
/**
|
|
153
208
|
* Tracks client disconnects so the polling loop can exit promptly.
|
|
154
209
|
*
|
|
@@ -165,11 +220,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
165
220
|
keepAliveInterval.unref?.();
|
|
166
221
|
|
|
167
222
|
try {
|
|
168
|
-
let
|
|
223
|
+
let shouldUseFullPolling = await emitLatestSnapshot();
|
|
169
224
|
|
|
170
225
|
while (!isStreamClosed && !request.signal.aborted) {
|
|
171
226
|
await waitForNextUserChatStreamPoll(
|
|
172
|
-
|
|
227
|
+
shouldUseFullPolling
|
|
173
228
|
? ACTIVE_USER_CHAT_STREAM_POLL_INTERVAL_MS
|
|
174
229
|
: IDLE_USER_CHAT_STREAM_POLL_INTERVAL_MS,
|
|
175
230
|
request.signal,
|
|
@@ -179,7 +234,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
179
234
|
break;
|
|
180
235
|
}
|
|
181
236
|
|
|
182
|
-
|
|
237
|
+
shouldUseFullPolling = shouldUseFullPolling
|
|
238
|
+
? await emitLatestSnapshot()
|
|
239
|
+
: await emitLatestSnapshotWhenRevisionChanged();
|
|
183
240
|
}
|
|
184
241
|
} catch (error) {
|
|
185
242
|
if (!isStreamClosed && !request.signal.aborted) {
|
|
@@ -210,6 +267,30 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
210
267
|
});
|
|
211
268
|
}
|
|
212
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Builds a compact signature for idle stream revision checks.
|
|
272
|
+
*/
|
|
273
|
+
function createUserChatRevisionSignature(revision: NonNullable<UserChatRevision>): string {
|
|
274
|
+
return JSON.stringify({
|
|
275
|
+
id: revision.id,
|
|
276
|
+
updatedAt: revision.updatedAt,
|
|
277
|
+
draftMessage: revision.draftMessage || '',
|
|
278
|
+
source: revision.source,
|
|
279
|
+
activeJobs: revision.activeJobs.map((job) => ({
|
|
280
|
+
id: job.id,
|
|
281
|
+
status: job.status,
|
|
282
|
+
cancelRequestedAt: job.cancelRequestedAt,
|
|
283
|
+
})),
|
|
284
|
+
activeTimeouts: revision.activeTimeouts.map((timeout) => ({
|
|
285
|
+
id: timeout.id,
|
|
286
|
+
status: timeout.status,
|
|
287
|
+
message: timeout.message || '',
|
|
288
|
+
dueAt: timeout.dueAt,
|
|
289
|
+
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
290
|
+
})),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
213
294
|
/**
|
|
214
295
|
* Builds a stable signature for the user-visible parts of a canonical chat snapshot.
|
|
215
296
|
*/
|
|
@@ -227,6 +308,7 @@ function createUserChatDetailSignature(payload: Awaited<ReturnType<typeof create
|
|
|
227
308
|
activeTimeouts: payload.activeTimeouts.map((timeout) => ({
|
|
228
309
|
id: timeout.id,
|
|
229
310
|
status: timeout.status,
|
|
311
|
+
message: timeout.message || '',
|
|
230
312
|
dueAt: timeout.dueAt,
|
|
231
313
|
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
232
314
|
})),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as dotenv from 'dotenv';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment variable pointing to the installed Agents Server `.env` file.
|
|
5
|
+
*
|
|
6
|
+
* @private utility of standalone database scripts
|
|
7
|
+
*/
|
|
8
|
+
export const AGENTS_SERVER_ENV_FILE_ENV_NAME = 'PTBK_AGENTS_SERVER_ENV_FILE';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loads Agents Server environment variables for standalone database scripts.
|
|
12
|
+
*
|
|
13
|
+
* The installer passes an explicit `.env` file from the installation directory. That file must override ambient
|
|
14
|
+
* `PTBK_*` values because these scripts often run from the repository checkout while the server runs from the
|
|
15
|
+
* installation directory, so a stale relative SQLite path would point at a different database.
|
|
16
|
+
*
|
|
17
|
+
* @private utility of standalone database scripts
|
|
18
|
+
*/
|
|
19
|
+
export function loadAgentsServerEnvFile(): void {
|
|
20
|
+
const explicitEnvFilePath = process.env[AGENTS_SERVER_ENV_FILE_ENV_NAME]?.trim();
|
|
21
|
+
if (explicitEnvFilePath) {
|
|
22
|
+
const explicitLoadResult = dotenv.config({ path: explicitEnvFilePath, override: true });
|
|
23
|
+
if (!explicitLoadResult.error) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dotenv.config();
|
|
29
|
+
}
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import colors from 'colors';
|
|
2
|
-
import * as dotenv from 'dotenv';
|
|
3
2
|
import {
|
|
4
3
|
DATABASE_MIGRATION_APPLIED_BY,
|
|
5
4
|
resolveDatabaseMigrationRuntimeConfiguration,
|
|
6
5
|
runDatabaseMigrations,
|
|
7
6
|
} from './runDatabaseMigrations';
|
|
8
7
|
import { isAgentsServerSqliteMode } from './agentsServerDatabaseMode';
|
|
8
|
+
import { loadAgentsServerEnvFile } from './loadAgentsServerEnvFile';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
* Environment variable pointing to the installed Agents Server `.env` file.
|
|
12
|
-
*/
|
|
13
|
-
const AGENTS_SERVER_ENV_FILE_ENV_NAME = 'PTBK_AGENTS_SERVER_ENV_FILE';
|
|
14
|
-
|
|
15
|
-
loadDatabaseMigrationEnvironment();
|
|
10
|
+
loadAgentsServerEnvFile();
|
|
16
11
|
|
|
17
12
|
/**
|
|
18
13
|
* Runs manual migration command from CLI arguments.
|
|
@@ -57,24 +52,6 @@ async function migrate(): Promise<void> {
|
|
|
57
52
|
}
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
/**
|
|
61
|
-
* Loads database migration environment variables.
|
|
62
|
-
*
|
|
63
|
-
* Self-update runs the migration command from the repository checkout, while
|
|
64
|
-
* the deployed Agents Server `.env` lives in the installation directory.
|
|
65
|
-
*/
|
|
66
|
-
function loadDatabaseMigrationEnvironment(): void {
|
|
67
|
-
const explicitEnvFilePath = process.env[AGENTS_SERVER_ENV_FILE_ENV_NAME]?.trim();
|
|
68
|
-
if (explicitEnvFilePath) {
|
|
69
|
-
const explicitLoadResult = dotenv.config({ path: explicitEnvFilePath });
|
|
70
|
-
if (!explicitLoadResult.error) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
dotenv.config();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
55
|
/**
|
|
79
56
|
* Parses optional `--only` flag from CLI arguments.
|
|
80
57
|
*
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import * as dotenv from 'dotenv';
|
|
2
1
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
2
|
import { AgentCollectionInSupabase } from '../../../../src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase';
|
|
4
3
|
import type { AgentsDatabaseSchema } from '../../../../src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema';
|
|
5
4
|
import { $provideSupabaseForServer } from './$provideSupabaseForServer';
|
|
6
5
|
import { DEFAULT_AGENT_VISIBILITY } from '../utils/agentVisibility';
|
|
7
6
|
import { loadDefaultAgentBooks } from '../utils/defaultAgents/loadDefaultAgentBooks';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Environment variable pointing to the installed Agents Server `.env` file.
|
|
11
|
-
*
|
|
12
|
-
* @private utility of standalone default-agent seeding
|
|
13
|
-
*/
|
|
14
|
-
const AGENTS_SERVER_ENV_FILE_ENV_NAME = 'PTBK_AGENTS_SERVER_ENV_FILE';
|
|
7
|
+
import { loadAgentsServerEnvFile } from './loadAgentsServerEnvFile';
|
|
15
8
|
|
|
16
9
|
/**
|
|
17
10
|
* Environment variable with an explicit default-agent source directory.
|
|
@@ -178,30 +171,13 @@ function resolveAgentsDatabaseSupabaseClient(): SupabaseClient<AgentsDatabaseSch
|
|
|
178
171
|
return $provideSupabaseForServer() as unknown as SupabaseClient<AgentsDatabaseSchema>;
|
|
179
172
|
}
|
|
180
173
|
|
|
181
|
-
/**
|
|
182
|
-
* Loads the installed Agents Server environment before seeding.
|
|
183
|
-
*
|
|
184
|
-
* @private utility of standalone default-agent seeding
|
|
185
|
-
*/
|
|
186
|
-
function loadSeedDefaultAgentsEnvironment(): void {
|
|
187
|
-
const explicitEnvFilePath = process.env[AGENTS_SERVER_ENV_FILE_ENV_NAME]?.trim();
|
|
188
|
-
if (explicitEnvFilePath) {
|
|
189
|
-
const explicitLoadResult = dotenv.config({ path: explicitEnvFilePath });
|
|
190
|
-
if (!explicitLoadResult.error) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
dotenv.config();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
174
|
/**
|
|
199
175
|
* Runs the standalone default-agent seed command.
|
|
200
176
|
*
|
|
201
177
|
* @private utility of standalone default-agent seeding
|
|
202
178
|
*/
|
|
203
179
|
async function runSeedDefaultAgentsCommand(): Promise<void> {
|
|
204
|
-
|
|
180
|
+
loadAgentsServerEnvFile();
|
|
205
181
|
const result = await seedDefaultAgents();
|
|
206
182
|
|
|
207
183
|
if (result.createdCount > 0) {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { $provideServer } from '../../tools/$provideServer';
|
|
2
2
|
import { $provideAgentCollectionForServer } from '../../tools/$provideAgentCollectionForServer';
|
|
3
|
+
import { $getTableName } from '../../database/$getTableName';
|
|
4
|
+
import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
|
|
3
5
|
import { $provideAgentReferenceResolver } from '../agentReferenceResolver/$provideAgentReferenceResolver';
|
|
4
6
|
import { consumeAgentReferenceResolutionIssues } from '../agentReferenceResolver/AgentReferenceResolutionIssue';
|
|
5
7
|
import { parseBookScopedAgentIdentifier } from '../agentReferenceResolver/bookScopedAgentReferences';
|
|
8
|
+
import { buildAgentNameOrIdFilter } from '../agentIdentifier';
|
|
6
9
|
import { resolvePseudoAgentDescriptor } from '../pseudoAgents';
|
|
7
10
|
import { normalizeAgentName } from '../../../../../src/_packages/core.index';
|
|
8
11
|
import type { PseudoAgentKind } from '../../../../../src/book-2.0/agent-source/pseudoAgentReferences';
|
|
12
|
+
import type { AgentsServerDatabase } from '../../database/schema';
|
|
9
13
|
import { cache } from 'react';
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -50,6 +54,14 @@ type PseudoAgentRouteTarget = {
|
|
|
50
54
|
canonicalUrl: string;
|
|
51
55
|
};
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Minimal database row needed to resolve one local route target.
|
|
59
|
+
*/
|
|
60
|
+
type LocalAgentRouteRow = Pick<
|
|
61
|
+
AgentsServerDatabase['public']['Tables']['Agent']['Row'],
|
|
62
|
+
'agentName' | 'permanentId'
|
|
63
|
+
>;
|
|
64
|
+
|
|
53
65
|
/**
|
|
54
66
|
* Target returned for an incoming `/agents/:agentId` route value.
|
|
55
67
|
*/
|
|
@@ -102,6 +114,11 @@ async function resolveAgentRouteTargetUncached(
|
|
|
102
114
|
};
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
const directLocalRouteTarget = await resolveLocalAgentRouteTargetFromDatabase(normalizedReference, localServerUrl);
|
|
118
|
+
if (directLocalRouteTarget) {
|
|
119
|
+
return directLocalRouteTarget;
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
const resolver = await $provideAgentReferenceResolver({ forceRefresh: options?.forceRefresh });
|
|
106
123
|
let resolvedUrlValue: string;
|
|
107
124
|
|
|
@@ -299,3 +316,42 @@ async function resolveLocalAgentRouteTarget(
|
|
|
299
316
|
canonicalUrl: `${localServerUrl}${AGENT_PATH_PREFIX}${encodeURIComponent(canonicalAgentId)}`,
|
|
300
317
|
};
|
|
301
318
|
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Resolves the common local route case with a targeted indexed database lookup.
|
|
322
|
+
*
|
|
323
|
+
* The reference resolver has to initialize the full local/federated lookup map.
|
|
324
|
+
* Most page requests already carry a permanent id or exact local name, so checking
|
|
325
|
+
* the Agent table first keeps normal navigation from loading every agent profile.
|
|
326
|
+
*
|
|
327
|
+
* @param reference - Normalized route reference.
|
|
328
|
+
* @param localServerUrl - Normalized URL of the current Agents Server instance.
|
|
329
|
+
* @returns Local route target or `null` when the reference is not a local agent.
|
|
330
|
+
*/
|
|
331
|
+
async function resolveLocalAgentRouteTargetFromDatabase(
|
|
332
|
+
reference: string,
|
|
333
|
+
localServerUrl: string,
|
|
334
|
+
): Promise<AgentRouteTarget | null> {
|
|
335
|
+
const supabase = $provideSupabaseForServer();
|
|
336
|
+
const agentTable = await $getTableName('Agent');
|
|
337
|
+
const result = await supabase
|
|
338
|
+
.from(agentTable)
|
|
339
|
+
.select('agentName, permanentId')
|
|
340
|
+
.or(buildAgentNameOrIdFilter(reference))
|
|
341
|
+
.is('deletedAt', null)
|
|
342
|
+
.order('createdAt', { ascending: true })
|
|
343
|
+
.limit(1);
|
|
344
|
+
|
|
345
|
+
if (result.error || !result.data || result.data.length === 0) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const agent = result.data[0] as LocalAgentRouteRow;
|
|
350
|
+
const canonicalAgentId = agent.permanentId || agent.agentName;
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
kind: 'local',
|
|
354
|
+
canonicalAgentId,
|
|
355
|
+
canonicalUrl: `${localServerUrl}${AGENT_PATH_PREFIX}${encodeURIComponent(canonicalAgentId)}`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -39,6 +39,16 @@ type ImportedAgentCacheRecord = {
|
|
|
39
39
|
*/
|
|
40
40
|
readonly source: string_book;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Fetch implementation that produced this cached record.
|
|
44
|
+
*/
|
|
45
|
+
readonly fetchImplementation: typeof fetch;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Timestamp when the source was last fetched successfully.
|
|
49
|
+
*/
|
|
50
|
+
readonly cachedAt: number;
|
|
51
|
+
|
|
42
52
|
/**
|
|
43
53
|
* Last observed ETag returned by the remote `/api/book` endpoint.
|
|
44
54
|
*/
|
|
@@ -55,6 +65,22 @@ type ImportedAgentCacheRecord = {
|
|
|
55
65
|
*/
|
|
56
66
|
const IMPORTED_AGENT_CACHE = new Map<string, ImportedAgentCacheRecord>();
|
|
57
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Freshness window for successful imported agent books.
|
|
70
|
+
*
|
|
71
|
+
* This avoids a remote revalidation round trip on every server render while
|
|
72
|
+
* still allowing federated/default parent updates to appear shortly after.
|
|
73
|
+
*/
|
|
74
|
+
const IMPORTED_AGENT_CACHE_FRESH_TTL_MS = 60_000;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Upper bound for fetching one imported agent book.
|
|
78
|
+
*
|
|
79
|
+
* Imported agents are used while rendering agent profiles and lists, so one slow
|
|
80
|
+
* federated/default parent must not block the entire Agents Server page render.
|
|
81
|
+
*/
|
|
82
|
+
const IMPORTED_AGENT_BOOK_FETCH_TIMEOUT_MS = 3_000;
|
|
83
|
+
|
|
58
84
|
/**
|
|
59
85
|
* In-flight remote imports deduplicated by canonical agent identifier.
|
|
60
86
|
*/
|
|
@@ -101,6 +127,26 @@ function createImportCacheKey(
|
|
|
101
127
|
return agentIdentification.replace(/\/+$/g, '');
|
|
102
128
|
}
|
|
103
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Returns `true` when a cached imported agent can be reused without revalidation.
|
|
132
|
+
*
|
|
133
|
+
* @param cachedImport - Cached successful import record.
|
|
134
|
+
* @returns Whether the cached import is still fresh enough for immediate reuse.
|
|
135
|
+
*/
|
|
136
|
+
function isImportedAgentCacheFresh(cachedImport: ImportedAgentCacheRecord): boolean {
|
|
137
|
+
return Date.now() - cachedImport.cachedAt < IMPORTED_AGENT_CACHE_FRESH_TTL_MS;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns `true` when the current fetch implementation can safely reuse the cached import.
|
|
142
|
+
*
|
|
143
|
+
* @param cachedImport - Cached successful import record.
|
|
144
|
+
* @returns Whether the cache was produced by the active fetch implementation.
|
|
145
|
+
*/
|
|
146
|
+
function isImportedAgentCacheCompatible(cachedImport: ImportedAgentCacheRecord): boolean {
|
|
147
|
+
return cachedImport.fetchImplementation === globalThis.fetch;
|
|
148
|
+
}
|
|
149
|
+
|
|
104
150
|
/**
|
|
105
151
|
* Extracts one text/book payload from a successful HTTP response.
|
|
106
152
|
*
|
|
@@ -153,7 +199,14 @@ export async function importAgent(
|
|
|
153
199
|
inheritancePath: options?.inheritancePath,
|
|
154
200
|
} satisfies ImportAgentOptions;
|
|
155
201
|
const cacheKey = createImportCacheKey(agentIdentification);
|
|
156
|
-
const
|
|
202
|
+
const storedCachedImport = IMPORTED_AGENT_CACHE.get(cacheKey);
|
|
203
|
+
const cachedImport =
|
|
204
|
+
storedCachedImport && isImportedAgentCacheCompatible(storedCachedImport) ? storedCachedImport : undefined;
|
|
205
|
+
|
|
206
|
+
if (cachedImport && isImportedAgentCacheFresh(cachedImport)) {
|
|
207
|
+
return cachedImport.source;
|
|
208
|
+
}
|
|
209
|
+
|
|
157
210
|
const existingRequest = PENDING_IMPORTED_AGENT_REQUESTS.get(cacheKey);
|
|
158
211
|
if (existingRequest) {
|
|
159
212
|
return existingRequest;
|
|
@@ -175,6 +228,7 @@ export async function importAgent(
|
|
|
175
228
|
const response: Response = await fetch(agentBookUrl, {
|
|
176
229
|
cache: 'no-store',
|
|
177
230
|
headers,
|
|
231
|
+
signal: AbortSignal.timeout(IMPORTED_AGENT_BOOK_FETCH_TIMEOUT_MS),
|
|
178
232
|
});
|
|
179
233
|
|
|
180
234
|
if (response.status === 304 && cachedImport) {
|
|
@@ -202,6 +256,8 @@ export async function importAgent(
|
|
|
202
256
|
const source = await readImportedAgentSource(agentIdentification, response);
|
|
203
257
|
IMPORTED_AGENT_CACHE.set(cacheKey, {
|
|
204
258
|
source,
|
|
259
|
+
fetchImplementation: globalThis.fetch,
|
|
260
|
+
cachedAt: Date.now(),
|
|
205
261
|
etag: response.headers.get('etag'),
|
|
206
262
|
lastModified: response.headers.get('last-modified'),
|
|
207
263
|
});
|
|
@@ -19,6 +19,10 @@ type FailedImportedAgentFallbackCacheRecord = {
|
|
|
19
19
|
* Generated fallback book returned for the failed import.
|
|
20
20
|
*/
|
|
21
21
|
readonly fallbackSource: string_book;
|
|
22
|
+
/**
|
|
23
|
+
* Fetch implementation that produced this failed-import fallback.
|
|
24
|
+
*/
|
|
25
|
+
readonly fetchImplementation: typeof fetch;
|
|
22
26
|
/**
|
|
23
27
|
* Cache expiration timestamp in epoch milliseconds.
|
|
24
28
|
*/
|
|
@@ -122,6 +126,11 @@ function readCachedFailedImportedAgentFallback(cacheKey: string): string_book |
|
|
|
122
126
|
return null;
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
if (cachedFallback.fetchImplementation !== globalThis.fetch) {
|
|
130
|
+
cachedFailedImportedAgentFallbackByKey.delete(cacheKey);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
return cachedFallback.fallbackSource;
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -134,6 +143,7 @@ function readCachedFailedImportedAgentFallback(cacheKey: string): string_book |
|
|
|
134
143
|
function writeCachedFailedImportedAgentFallback(cacheKey: string, fallbackSource: string_book): void {
|
|
135
144
|
cachedFailedImportedAgentFallbackByKey.set(cacheKey, {
|
|
136
145
|
fallbackSource,
|
|
146
|
+
fetchImplementation: globalThis.fetch,
|
|
137
147
|
expiresAt: Date.now() + FAILED_IMPORTED_AGENT_FALLBACK_CACHE_TTL_MS,
|
|
138
148
|
});
|
|
139
149
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { UserChatRow } from './UserChatRow';
|
|
2
|
+
import { provideUserChatTable } from './provideUserChatTable';
|
|
3
|
+
import type { UserChatJobRow } from './UserChatJobRow';
|
|
4
|
+
import { provideUserChatJobTable } from './provideUserChatJobTable';
|
|
5
|
+
import { ACTIVE_USER_CHAT_TIMEOUT_STATUSES } from '../userChatTimeout/userChatTimeoutStore/ACTIVE_USER_CHAT_TIMEOUT_STATUSES';
|
|
6
|
+
import { isMissingUserChatTimeoutRelationError } from '../userChatTimeout/userChatTimeoutStore/isMissingUserChatTimeoutRelationError';
|
|
7
|
+
import { provideUserChatTimeoutTable } from '../userChatTimeout/userChatTimeoutStore/provideUserChatTimeoutTable';
|
|
8
|
+
import type { UserChatTimeoutRow } from '../userChatTimeout/UserChatTimeoutRecord';
|
|
9
|
+
import { USER_CHAT_SOURCES } from './UserChatSource';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Active user-chat job statuses that keep the live chat stream in a faster polling mode.
|
|
13
|
+
*/
|
|
14
|
+
const ACTIVE_USER_CHAT_JOB_STATUSES = ['QUEUED', 'RUNNING'] as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compact active-job shape used only for revision comparison.
|
|
18
|
+
*/
|
|
19
|
+
export type UserChatRevisionJob = Pick<UserChatJobRow, 'id' | 'status' | 'cancelRequestedAt'>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compact active-timeout shape used only for revision comparison.
|
|
23
|
+
*/
|
|
24
|
+
export type UserChatRevisionTimeout = Pick<
|
|
25
|
+
UserChatTimeoutRow,
|
|
26
|
+
'id' | 'status' | 'message' | 'dueAt' | 'cancelRequestedAt'
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lightweight revision metadata for detecting whether a chat detail payload changed.
|
|
31
|
+
*/
|
|
32
|
+
export type UserChatRevision = {
|
|
33
|
+
id: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
draftMessage: string | null;
|
|
36
|
+
source: UserChatRow['source'];
|
|
37
|
+
activeJobs: Array<UserChatRevisionJob>;
|
|
38
|
+
activeTimeouts: Array<UserChatRevisionTimeout>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Loads one scoped chat revision without hydrating the full `messages` JSON payload.
|
|
43
|
+
*/
|
|
44
|
+
export async function getUserChatRevision(options: {
|
|
45
|
+
userId: number;
|
|
46
|
+
viewerIsAdmin?: boolean;
|
|
47
|
+
agentPermanentId: string;
|
|
48
|
+
chatId: string;
|
|
49
|
+
}): Promise<UserChatRevision | null> {
|
|
50
|
+
const { userId, viewerIsAdmin = false, agentPermanentId, chatId } = options;
|
|
51
|
+
const userChatTable = await provideUserChatTable();
|
|
52
|
+
|
|
53
|
+
const { data, error } = await userChatTable
|
|
54
|
+
.select('id, userId, updatedAt, draftMessage, source')
|
|
55
|
+
.eq('id', chatId)
|
|
56
|
+
.eq('agentPermanentId', agentPermanentId)
|
|
57
|
+
.maybeSingle();
|
|
58
|
+
|
|
59
|
+
if (error) {
|
|
60
|
+
throw new Error(`Failed to load user chat revision "${chatId}": ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!data) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const row = data as Pick<UserChatRow, 'id' | 'userId' | 'updatedAt' | 'draftMessage' | 'source'>;
|
|
68
|
+
if (row.source === USER_CHAT_SOURCES.WEB_UI) {
|
|
69
|
+
if (row.userId !== userId) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
} else if (!viewerIsAdmin) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const [activeJobs, activeTimeouts] = await Promise.all([
|
|
77
|
+
listUserChatRevisionActiveJobs({ userId: row.userId, agentPermanentId, chatId }),
|
|
78
|
+
listUserChatRevisionActiveTimeouts({ userId: row.userId, agentPermanentId, chatId }),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id: row.id,
|
|
83
|
+
updatedAt: row.updatedAt,
|
|
84
|
+
draftMessage: row.draftMessage,
|
|
85
|
+
source: row.source,
|
|
86
|
+
activeJobs,
|
|
87
|
+
activeTimeouts,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Lists only active job fields that affect the canonical chat surface.
|
|
93
|
+
*/
|
|
94
|
+
async function listUserChatRevisionActiveJobs(options: {
|
|
95
|
+
userId: number;
|
|
96
|
+
agentPermanentId: string;
|
|
97
|
+
chatId: string;
|
|
98
|
+
}): Promise<Array<UserChatRevisionJob>> {
|
|
99
|
+
const userChatJobTable = await provideUserChatJobTable();
|
|
100
|
+
const { data, error } = await userChatJobTable
|
|
101
|
+
.select('id, status, cancelRequestedAt')
|
|
102
|
+
.eq('chatId', options.chatId)
|
|
103
|
+
.eq('userId', options.userId)
|
|
104
|
+
.eq('agentPermanentId', options.agentPermanentId)
|
|
105
|
+
.in('status', ACTIVE_USER_CHAT_JOB_STATUSES)
|
|
106
|
+
.order('createdAt', { ascending: true });
|
|
107
|
+
|
|
108
|
+
if (error) {
|
|
109
|
+
throw new Error(`Failed to load active user chat job revisions for chat "${options.chatId}": ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (data || []) as Array<UserChatRevisionJob>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Lists only active timeout fields that affect the canonical chat surface.
|
|
117
|
+
*/
|
|
118
|
+
async function listUserChatRevisionActiveTimeouts(options: {
|
|
119
|
+
userId: number;
|
|
120
|
+
agentPermanentId: string;
|
|
121
|
+
chatId: string;
|
|
122
|
+
}): Promise<Array<UserChatRevisionTimeout>> {
|
|
123
|
+
const userChatTimeoutTable = await provideUserChatTimeoutTable();
|
|
124
|
+
const { data, error } = await userChatTimeoutTable
|
|
125
|
+
.select('id, status, message, dueAt, cancelRequestedAt')
|
|
126
|
+
.eq('chatId', options.chatId)
|
|
127
|
+
.eq('userId', options.userId)
|
|
128
|
+
.eq('agentPermanentId', options.agentPermanentId)
|
|
129
|
+
.in('status', ACTIVE_USER_CHAT_TIMEOUT_STATUSES)
|
|
130
|
+
.is('pausedAt', null)
|
|
131
|
+
.order('dueAt', { ascending: true })
|
|
132
|
+
.order('createdAt', { ascending: true });
|
|
133
|
+
|
|
134
|
+
if (error) {
|
|
135
|
+
if (isMissingUserChatTimeoutRelationError(error)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Failed to load active user chat timeout revisions for chat "${options.chatId}": ${error.message}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (data || []) as Array<UserChatRevisionTimeout>;
|
|
145
|
+
}
|
|
@@ -13,6 +13,7 @@ export { getUserChat } from './userChat/getUserChat';
|
|
|
13
13
|
export { getUserChatJob } from './userChat/getUserChatJob';
|
|
14
14
|
export { getUserChatJobById } from './userChat/getUserChatJobById';
|
|
15
15
|
export { getUserChatJobByClientMessageId } from './userChat/getUserChatJobByClientMessageId';
|
|
16
|
+
export { getUserChatRevision } from './userChat/getUserChatRevision';
|
|
16
17
|
export { heartbeatUserChatJob } from './userChat/heartbeatUserChatJob';
|
|
17
18
|
export { listExpiredRunningUserChatJobs } from './userChat/listExpiredRunningUserChatJobs';
|
|
18
19
|
export { listUserChats, listUserChatSummarySeeds } from './userChat/listUserChats';
|