@promptbook/cli 0.112.0-113 → 0.112.0-114

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.
Files changed (27) hide show
  1. package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts +85 -56
  2. package/apps/agents-server/src/app/agents/[agentName]/chat/useAgentChatHistorySyncEffects.ts +7 -13
  3. package/apps/agents-server/src/database/migrations/2026-06-1300-user-chat-active-read-indexes.sql +7 -0
  4. package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +38 -0
  5. package/apps/agents-server/src/utils/userChat/createImmediateUserChatAnswerModelRequirements.ts +15 -12
  6. package/apps/agents-server/src/utils/userChat/createUserChatDetailPayload.ts +33 -18
  7. package/apps/agents-server/src/utils/userChat/hasPotentiallyPendingAssistantMessages.ts +26 -0
  8. package/apps/agents-server/src/utils/userChat/runImmediateUserChatAnswer.ts +1 -1
  9. package/esm/index.es.js +60 -24
  10. package/esm/index.es.js.map +1 -1
  11. package/esm/scripts/run-codex-prompts/common/runGoScript/printLiveScriptChunk.d.ts +4 -0
  12. package/esm/src/cli/cli-commands/agent/agentCliOptions.d.ts +10 -1
  13. package/esm/src/version.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/book-3.0/LiteAgent.ts +15 -10
  16. package/src/cli/cli-commands/agent/agentCliOptions.ts +33 -4
  17. package/src/cli/cli-commands/agent/chat.ts +2 -2
  18. package/src/cli/cli-commands/agent/exec.ts +2 -2
  19. package/src/cli/cli-commands/agent.ts +0 -1
  20. package/src/other/templates/getTemplatesPipelineCollection.ts +767 -853
  21. package/src/version.ts +2 -2
  22. package/src/versions.txt +1 -0
  23. package/umd/index.umd.js +60 -24
  24. package/umd/index.umd.js.map +1 -1
  25. package/umd/scripts/run-codex-prompts/common/runGoScript/printLiveScriptChunk.d.ts +4 -0
  26. package/umd/src/cli/cli-commands/agent/agentCliOptions.d.ts +10 -1
  27. package/umd/src/version.d.ts +1 -1
@@ -1,7 +1,13 @@
1
1
  import { CHAT_STREAM_KEEP_ALIVE_INTERVAL_MS } from '@/src/constants/streaming';
2
2
  import { isPrivateModeEnabledFromRequest } from '@/src/utils/privateMode';
3
- import { createUserChatDetailPayload, getUserChat, isFrozenUserChatSource } from '@/src/utils/userChat';
4
- import type { ChatMessage } from '@promptbook-local/types';
3
+ import {
4
+ createUserChatDetailPayload,
5
+ getUserChat,
6
+ isFrozenUserChatSource,
7
+ listUserChatJobs,
8
+ } from '@/src/utils/userChat';
9
+ import { hasPotentiallyPendingAssistantMessages } from '@/src/utils/userChat/hasPotentiallyPendingAssistantMessages';
10
+ import { listUserChatTimeouts } from '@/src/utils/userChatTimeout/userChatTimeoutStore';
5
11
  import { NextResponse } from 'next/server';
6
12
  import { resolveUserChatScope } from '../../resolveUserChatScope';
7
13
 
@@ -136,17 +142,17 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
136
142
  return false;
137
143
  }
138
144
 
139
- const payload = await createUserChatDetailPayload(currentChat);
140
- const nextSignature = createUserChatDetailSignature(payload);
145
+ const livePollState = await loadLiveUserChatPollingState(currentChat);
141
146
 
142
- if (nextSignature !== lastSnapshotSignature) {
143
- lastSnapshotSignature = nextSignature;
147
+ if (livePollState.signature !== lastSnapshotSignature) {
148
+ const payload = await createUserChatDetailPayload(currentChat);
149
+ lastSnapshotSignature = createUserChatPollingSignatureFromDetailPayload(payload);
144
150
  if (!enqueueFrame({ type: 'snapshot', payload })) {
145
151
  return false;
146
152
  }
147
153
  }
148
154
 
149
- return !isFrozenUserChatSource(payload.chat.source) && payload.activeJobs.length > 0;
155
+ return !isFrozenUserChatSource(currentChat.source) && livePollState.hasActiveJobs;
150
156
  };
151
157
 
152
158
  /**
@@ -211,69 +217,92 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
211
217
  }
212
218
 
213
219
  /**
214
- * Builds a stable signature for the user-visible parts of a canonical chat snapshot.
220
+ * Minimal live poll state used to avoid rebuilding the full chat payload when nothing visible changed.
215
221
  */
216
- function createUserChatDetailSignature(payload: Awaited<ReturnType<typeof createUserChatDetailPayload>>): string {
217
- return JSON.stringify({
218
- chatId: payload.chat.id,
219
- updatedAt: payload.chat.updatedAt,
220
- draftMessage: payload.draftMessage || '',
221
- messages: payload.messages.map(createUserChatMessageSignature),
222
- activeJobs: payload.activeJobs.map((job) => ({
223
- id: job.id,
224
- status: job.status,
225
- cancelRequestedAt: job.cancelRequestedAt,
226
- })),
227
- activeTimeouts: payload.activeTimeouts.map((timeout) => ({
228
- id: timeout.id,
229
- status: timeout.status,
230
- dueAt: timeout.dueAt,
231
- cancelRequestedAt: timeout.cancelRequestedAt,
232
- })),
233
- });
234
- }
222
+ type LiveUserChatPollState = {
223
+ signature: string;
224
+ hasActiveJobs: boolean;
225
+ };
235
226
 
236
227
  /**
237
- * Builds one compact stable signature for a user-visible chat message.
228
+ * Loads the lightweight state needed to decide whether the active chat payload changed.
229
+ *
230
+ * The expensive detail payload does job reconciliation, local-runner synchronization, and full
231
+ * transcript serialization. Polling first with the persisted timestamps + active resource state
232
+ * keeps long-lived chat streams cheap when the conversation is idle.
233
+ *
234
+ * @param chat - Current scoped chat record.
235
+ * @returns Poll signature plus whether the chat should keep using the active-job cadence.
238
236
  */
239
- function createUserChatMessageSignature(
240
- message: Awaited<ReturnType<typeof createUserChatDetailPayload>>['messages'][number],
241
- ): Record<string, unknown> {
237
+ async function loadLiveUserChatPollingState(chat: NonNullable<Awaited<ReturnType<typeof getUserChat>>>): Promise<LiveUserChatPollState> {
238
+ const shouldInspectActiveJobs = hasPotentiallyPendingAssistantMessages(chat.messages);
239
+ const [activeJobs, activeTimeouts] = await Promise.all([
240
+ shouldInspectActiveJobs
241
+ ? listUserChatJobs({
242
+ userId: chat.userId,
243
+ agentPermanentId: chat.agentPermanentId,
244
+ chatId: chat.id,
245
+ onlyActive: true,
246
+ })
247
+ : Promise.resolve([]),
248
+ listUserChatTimeouts({
249
+ userId: chat.userId,
250
+ agentPermanentId: chat.agentPermanentId,
251
+ chatId: chat.id,
252
+ onlyActive: true,
253
+ }),
254
+ ]);
255
+
242
256
  return {
243
- id: message.id ?? null,
244
- sender: message.sender,
245
- isComplete: message.isComplete,
246
- lifecycleState: message.lifecycleState ?? null,
247
- lifecycleError: message.lifecycleError ?? null,
248
- contentLength: message.content.length,
249
- contentHash: createStableTextDigest(message.content),
250
- replyingTo: message.replyingTo ? JSON.stringify(message.replyingTo) : null,
251
- progressCard: message.progressCard ? JSON.stringify(message.progressCard) : null,
252
- ongoingToolCalls: createToolCallsSignature(message.ongoingToolCalls),
253
- completedToolCalls: createToolCallsSignature(message.completedToolCalls),
254
- toolCalls: createToolCallsSignature(message.toolCalls),
257
+ signature: createUserChatPollingSignature({
258
+ chatUpdatedAt: chat.updatedAt,
259
+ draftMessage: chat.draftMessage,
260
+ activeJobs,
261
+ activeTimeouts,
262
+ }),
263
+ hasActiveJobs: activeJobs.length > 0,
255
264
  };
256
265
  }
257
266
 
258
267
  /**
259
- * Creates one compact stable digest for message text without pulling in heavier hashing helpers.
268
+ * Builds the polling signature for a fully hydrated detail payload.
260
269
  */
261
- function createStableTextDigest(value: string): string {
262
- let hash = 2_166_136_261;
263
-
264
- for (let index = 0; index < value.length; index++) {
265
- hash ^= value.charCodeAt(index);
266
- hash = Math.imul(hash, 16_777_619);
267
- }
268
-
269
- return (hash >>> 0).toString(16);
270
+ function createUserChatPollingSignatureFromDetailPayload(
271
+ payload: Awaited<ReturnType<typeof createUserChatDetailPayload>>,
272
+ ): string {
273
+ return createUserChatPollingSignature({
274
+ chatUpdatedAt: payload.chat.updatedAt,
275
+ draftMessage: payload.draftMessage,
276
+ activeJobs: payload.activeJobs,
277
+ activeTimeouts: payload.activeTimeouts,
278
+ });
270
279
  }
271
280
 
272
281
  /**
273
- * Serializes optional tool-call arrays for snapshot signature comparisons.
282
+ * Builds a stable signature for the user-visible state that changes stream snapshots.
274
283
  */
275
- function createToolCallsSignature(toolCalls: ChatMessage['toolCalls']): string | null {
276
- return toolCalls && toolCalls.length > 0 ? JSON.stringify(toolCalls) : null;
284
+ function createUserChatPollingSignature(options: {
285
+ chatUpdatedAt: string;
286
+ draftMessage: string | null;
287
+ activeJobs: ReadonlyArray<Awaited<ReturnType<typeof createUserChatDetailPayload>>['activeJobs'][number]>;
288
+ activeTimeouts: ReadonlyArray<Awaited<ReturnType<typeof createUserChatDetailPayload>>['activeTimeouts'][number]>;
289
+ }): string {
290
+ return JSON.stringify({
291
+ updatedAt: options.chatUpdatedAt,
292
+ draftMessage: options.draftMessage || '',
293
+ activeJobs: options.activeJobs.map((job) => ({
294
+ id: job.id,
295
+ status: job.status,
296
+ cancelRequestedAt: job.cancelRequestedAt,
297
+ })),
298
+ activeTimeouts: options.activeTimeouts.map((timeout) => ({
299
+ id: timeout.id,
300
+ status: timeout.status,
301
+ dueAt: timeout.dueAt,
302
+ cancelRequestedAt: timeout.cancelRequestedAt,
303
+ pausedAt: timeout.pausedAt,
304
+ })),
305
+ });
277
306
  }
278
307
 
279
308
  /**
@@ -22,13 +22,6 @@ const USER_CHAT_STREAM_RECONNECT_DELAY_MS = 1_500;
22
22
  */
23
23
  const DISCONNECTED_CHAT_REFRESH_INTERVAL_MS = 4_000;
24
24
 
25
- /**
26
- * Periodic sidebar/list refresh cadence while the active chat stream is healthy.
27
- *
28
- * @private function of useAgentChatHistoryClientState
29
- */
30
- const CHAT_LIST_REFRESH_INTERVAL_MS = 20_000;
31
-
32
25
  /**
33
26
  * Inputs required to register side effects for durable chat-history synchronization.
34
27
  *
@@ -504,7 +497,7 @@ function keepActiveChatStreamConnected(params: {
504
497
  }
505
498
 
506
499
  /**
507
- * Refreshes the selected chat periodically and on tab visibility/focus changes.
500
+ * Refreshes the selected chat on disconnect polling and on tab visibility/focus changes.
508
501
  *
509
502
  * @private function of useAgentChatHistoryClientState
510
503
  */
@@ -529,9 +522,6 @@ function registerActiveChatRefreshPolling(params: {
529
522
  return undefined;
530
523
  }
531
524
 
532
- const pollIntervalMs = isActiveChatStreamConnected
533
- ? CHAT_LIST_REFRESH_INTERVAL_MS
534
- : DISCONNECTED_CHAT_REFRESH_INTERVAL_MS;
535
525
  const runRefresh = () => {
536
526
  if (typeof document !== 'undefined' && document.hidden) {
537
527
  return;
@@ -540,7 +530,9 @@ function registerActiveChatRefreshPolling(params: {
540
530
  void refreshActiveChat({ preserveDirtyDraft: true });
541
531
  };
542
532
 
543
- const interval = window.setInterval(runRefresh, pollIntervalMs);
533
+ const interval = isActiveChatStreamConnected
534
+ ? null
535
+ : window.setInterval(runRefresh, DISCONNECTED_CHAT_REFRESH_INTERVAL_MS);
544
536
  const handleVisibilityChange = () => {
545
537
  if (typeof document !== 'undefined' && !document.hidden) {
546
538
  runRefresh();
@@ -554,7 +546,9 @@ function registerActiveChatRefreshPolling(params: {
554
546
  window.addEventListener('focus', handleFocus);
555
547
 
556
548
  return () => {
557
- window.clearInterval(interval);
549
+ if (interval !== null) {
550
+ window.clearInterval(interval);
551
+ }
558
552
  document.removeEventListener('visibilitychange', handleVisibilityChange);
559
553
  window.removeEventListener('focus', handleFocus);
560
554
  };
@@ -0,0 +1,7 @@
1
+ CREATE INDEX IF NOT EXISTS "prefix_UserChatJob_chatId_userId_agentPermanentId_active_createdAt_idx"
2
+ ON "prefix_UserChatJob" ("chatId", "userId", "agentPermanentId", "createdAt" ASC)
3
+ WHERE "status" IN ('QUEUED', 'RUNNING');
4
+
5
+ CREATE INDEX IF NOT EXISTS "prefix_UserChatTimeout_chatId_userId_agentPermanentId_active_dueAt_idx"
6
+ ON "prefix_UserChatTimeout" ("chatId", "userId", "agentPermanentId", "dueAt" ASC, "createdAt" ASC)
7
+ WHERE "status" IN ('QUEUED', 'RUNNING') AND "pausedAt" IS NULL;
@@ -102,6 +102,13 @@ async function resolveAgentRouteTargetUncached(
102
102
  };
103
103
  }
104
104
 
105
+ if (!options?.forceRefresh) {
106
+ const fastLocalRouteTarget = await resolveFastLocalAgentRouteTarget(normalizedReference, localServerUrl);
107
+ if (fastLocalRouteTarget) {
108
+ return fastLocalRouteTarget;
109
+ }
110
+ }
111
+
105
112
  const resolver = await $provideAgentReferenceResolver({ forceRefresh: options?.forceRefresh });
106
113
  let resolvedUrlValue: string;
107
114
 
@@ -145,6 +152,37 @@ async function resolveAgentRouteTargetUncached(
145
152
  };
146
153
  }
147
154
 
155
+ /**
156
+ * Resolves the common local-agent case without constructing the heavier shared reference resolver.
157
+ *
158
+ * Normal page and chat routes almost always target one local agent by its stored name or permanent id.
159
+ * Handling that case up front keeps route resolution cheap while still falling back to the full TEAM/federation
160
+ * resolver for aliases, remote agents, and fuzzy matches.
161
+ *
162
+ * @param reference - Normalized route/reference text.
163
+ * @param localServerUrl - Normalized URL of the current Agents Server instance.
164
+ * @returns Local route target or `null` when a direct lookup does not match.
165
+ */
166
+ async function resolveFastLocalAgentRouteTarget(
167
+ reference: string,
168
+ localServerUrl: string,
169
+ ): Promise<AgentRouteTarget | null> {
170
+ const collection = await $provideAgentCollectionForServer();
171
+ const directMatch = await collection.findAgentBasicInformation(reference);
172
+
173
+ if (!directMatch) {
174
+ return null;
175
+ }
176
+
177
+ const canonicalAgentId = directMatch.permanentId || directMatch.agentName;
178
+
179
+ return {
180
+ kind: 'local',
181
+ canonicalAgentId,
182
+ canonicalUrl: `${localServerUrl}${AGENT_PATH_PREFIX}${encodeURIComponent(canonicalAgentId)}`,
183
+ };
184
+ }
185
+
148
186
  /**
149
187
  * Memoized route-target resolver used for normal page rendering.
150
188
  */
@@ -5,7 +5,7 @@ import { parseAgentSourceWithCommitments } from '../../../../../src/book-2.0/age
5
5
  import type { string_book } from '../../../../../src/book-2.0/agent-source/string_book';
6
6
 
7
7
  /**
8
- * Commitments safe to use in the immediate answer without triggering slow tools, knowledge, imports, or memory.
8
+ * Commitments safe to use in the immediate pre-answer without triggering slow tools, knowledge, imports, or memory.
9
9
  */
10
10
  const IMMEDIATE_USER_CHAT_ANSWER_INSTRUCTION_COMMITMENTS = new Set<string>([
11
11
  'PERSONA',
@@ -26,27 +26,30 @@ const IMMEDIATE_USER_CHAT_ANSWER_INSTRUCTION_COMMITMENTS = new Set<string>([
26
26
  ]);
27
27
 
28
28
  /**
29
- * Prefix added to the immediate answer system message.
29
+ * Prefix added to the immediate pre-answer system message.
30
30
  */
31
31
  const IMMEDIATE_USER_CHAT_ANSWER_SYSTEM_PREAMBLE = spaceTrim(`
32
- You are preparing a fast draft answer for the user while a slower full agent run continues separately.
33
- This response is not the final answer.
34
- These immediate-answer rules override any agent instruction below that would make the answer sound final.
32
+ You are preparing a short in-progress confirmation for the user while a slower full agent run continues separately.
33
+ This response is not the final answer. It is only a confirmation that the task is being handled.
34
+ These immediate-answer rules override any agent instruction below that would make the answer sound final or complete.
35
35
 
36
36
  At the start of every response, clearly say in the user's language:
37
- - This is only a draft or preliminary answer, not the final answer.
38
- - The final answer will arrive in several minutes after the external service finishes processing.
39
- - The final answer can change or correct this draft, so the user should not treat this draft as final.
37
+ - You understood what the user wants.
38
+ - You are working on it now or the job has already started.
39
+ - The final answer will arrive after the background processing finishes.
40
40
 
41
- After that notice, give a brief useful draft of what is happening or the likely answer.
41
+ Keep the whole response short, preferably one or two sentences.
42
+ Do not provide any part of the final answer yet.
43
+ Do not include code snippets, detailed steps, calculations, drafted content, likely conclusions, or partial deliverables.
44
+ If helpful, briefly name the kind of work being done, such as writing code, checking something, preparing an answer, or generating an image.
42
45
  Answer directly and use only the instructions, conversation, attachments, and general model knowledge available in this request.
43
46
  Do not use or claim to have used external tools, memory, knowledge bases, web browsing, search, calendar, email, projects, or teammate agents.
44
- Never present this draft as complete or definitive.
45
- If the user asks for something that clearly requires those unavailable capabilities, explain that the checked final answer is still being prepared.
47
+ Never present this message as complete, definitive, or ready to use.
48
+ If the user asks for something that clearly requires unavailable capabilities, simply say the checked final answer is still being prepared.
46
49
  `);
47
50
 
48
51
  /**
49
- * Creates the lightweight model requirements used by the immediate answer path.
52
+ * Creates the lightweight model requirements used by the immediate pre-answer path.
50
53
  */
51
54
  export function createImmediateUserChatAnswerModelRequirements(
52
55
  agentSource: string_book,
@@ -1,5 +1,6 @@
1
1
  import type { UserChatRecord } from './UserChatRecord';
2
2
  import { synchronizeLocalUserChatJobsForChat } from '../localChatRunner/synchronizeLocalUserChatJobs';
3
+ import { hasPotentiallyPendingAssistantMessages } from './hasPotentiallyPendingAssistantMessages';
3
4
  import { createUserChatTimeoutActivity } from '../userChatTimeout/createUserChatTimeoutActivity';
4
5
  import { listUserChatTimeouts } from '../userChatTimeout/userChatTimeoutStore';
5
6
  import { createUserChatSummary } from './createUserChatSummary';
@@ -18,31 +19,45 @@ export async function createUserChatDetailPayload(chat: UserChatRecord): Promise
18
19
  activeTimeouts: Awaited<ReturnType<typeof listUserChatTimeouts>>;
19
20
  }> {
20
21
  let currentChat = chat;
21
- const hasSynchronizedLocalJobs = await synchronizeLocalUserChatJobsForChat(currentChat);
22
-
23
- if (hasSynchronizedLocalJobs) {
24
- const refreshedChat = await getUserChat({
25
- userId: currentChat.userId,
26
- agentPermanentId: currentChat.agentPermanentId,
27
- chatId: currentChat.id,
28
- });
29
-
30
- if (refreshedChat) {
31
- currentChat = refreshedChat;
32
- }
33
- }
34
-
35
22
  let activeJobs = await listUserChatJobs({
36
23
  userId: currentChat.userId,
37
24
  agentPermanentId: currentChat.agentPermanentId,
38
25
  chatId: currentChat.id,
39
26
  onlyActive: true,
40
27
  });
28
+ const shouldInspectJobState =
29
+ activeJobs.length > 0 || hasPotentiallyPendingAssistantMessages(currentChat.messages);
41
30
 
42
- const hasReconciledJobs = await reconcileUserChatActiveJobs({
43
- chat: currentChat,
44
- activeJobs,
45
- });
31
+ if (shouldInspectJobState) {
32
+ const hasSynchronizedLocalJobs = await synchronizeLocalUserChatJobsForChat(currentChat);
33
+
34
+ if (hasSynchronizedLocalJobs) {
35
+ const refreshedChat = await getUserChat({
36
+ userId: currentChat.userId,
37
+ agentPermanentId: currentChat.agentPermanentId,
38
+ chatId: currentChat.id,
39
+ });
40
+
41
+ if (refreshedChat) {
42
+ currentChat = refreshedChat;
43
+ }
44
+
45
+ activeJobs = await listUserChatJobs({
46
+ userId: currentChat.userId,
47
+ agentPermanentId: currentChat.agentPermanentId,
48
+ chatId: currentChat.id,
49
+ onlyActive: true,
50
+ });
51
+ }
52
+ }
53
+
54
+ const hasReconciledJobs =
55
+ activeJobs.length > 0
56
+ ? await reconcileUserChatActiveJobs({
57
+ chat: currentChat,
58
+ activeJobs,
59
+ })
60
+ : false;
46
61
 
47
62
  if (hasReconciledJobs) {
48
63
  const refreshedChat = await getUserChat({
@@ -0,0 +1,26 @@
1
+ import type { ChatMessage } from '@promptbook-local/types';
2
+
3
+ /**
4
+ * Returns `true` when the current transcript still suggests unfinished assistant work.
5
+ *
6
+ * This is intentionally message-based so readers can avoid heavier reconciliation work for
7
+ * settled chats while still inspecting threads that contain incomplete placeholders.
8
+ *
9
+ * @param messages - Current persisted chat transcript.
10
+ * @returns Whether the transcript still looks unfinished from the UI perspective.
11
+ * @private internal utility of `userChat`
12
+ */
13
+ export function hasPotentiallyPendingAssistantMessages(messages: ReadonlyArray<ChatMessage>): boolean {
14
+ return messages.some((message) => {
15
+ const sender = String(message.sender || '').toUpperCase();
16
+ if (sender !== 'AGENT' && sender !== 'MODEL') {
17
+ return false;
18
+ }
19
+
20
+ return (
21
+ message.isComplete === false ||
22
+ message.lifecycleState === 'queued' ||
23
+ message.lifecycleState === 'running'
24
+ );
25
+ });
26
+ }
@@ -44,7 +44,7 @@ type RunImmediateUserChatAnswerOptions = {
44
44
  };
45
45
 
46
46
  /**
47
- * Runs a fast local LLM answer into the incomplete assistant placeholder while the external runner is still working.
47
+ * Runs a fast local LLM pre-answer into the incomplete assistant placeholder while the external runner is still working.
48
48
  */
49
49
  export async function runImmediateUserChatAnswer(
50
50
  job: UserChatJobRecord,