@nocobase/app-plugin-agents 2.0.0-beta.0 → 2.0.0-beta.2
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/CHANGELOG.md +42 -0
- package/README.md +9 -1
- package/dist/client/chat/coalesced-invalidation.d.ts +3 -8
- package/dist/client/chat/coalesced-invalidation.d.ts.map +1 -1
- package/dist/client/chat/coalesced-invalidation.js +16 -21
- package/dist/client/chat/coalesced-invalidation.js.map +1 -1
- package/dist/client/chat/controller.d.ts +30 -3
- package/dist/client/chat/controller.d.ts.map +1 -1
- package/dist/client/chat/controller.js +472 -76
- package/dist/client/chat/controller.js.map +1 -1
- package/dist/client/chat/provider.d.ts.map +1 -1
- package/dist/client/chat/provider.js +7 -4
- package/dist/client/chat/provider.js.map +1 -1
- package/dist/client/chat/use-chat.d.ts +4 -2
- package/dist/client/chat/use-chat.d.ts.map +1 -1
- package/dist/client/chat/use-chat.js +82 -51
- package/dist/client/chat/use-chat.js.map +1 -1
- package/dist/client/hooks/use-realtime-topic.d.ts +1 -1
- package/dist/client/hooks/use-realtime-topic.d.ts.map +1 -1
- package/dist/client/hooks/use-realtime-topic.js +8 -4
- package/dist/client/hooks/use-realtime-topic.js.map +1 -1
- package/dist/client/kit.d.ts.map +1 -1
- package/dist/client/kit.js +30 -5
- package/dist/client/kit.js.map +1 -1
- package/dist/client/locales/de-DE.d.ts.map +1 -1
- package/dist/client/locales/de-DE.js +2 -0
- package/dist/client/locales/de-DE.js.map +1 -1
- package/dist/client/locales/es-ES.d.ts.map +1 -1
- package/dist/client/locales/es-ES.js +2 -0
- package/dist/client/locales/es-ES.js.map +1 -1
- package/dist/client/locales/fr-FR.d.ts.map +1 -1
- package/dist/client/locales/fr-FR.js +2 -0
- package/dist/client/locales/fr-FR.js.map +1 -1
- package/dist/client/locales/id-ID.d.ts.map +1 -1
- package/dist/client/locales/id-ID.js +2 -0
- package/dist/client/locales/id-ID.js.map +1 -1
- package/dist/client/locales/ja-JP.d.ts.map +1 -1
- package/dist/client/locales/ja-JP.js +2 -0
- package/dist/client/locales/ja-JP.js.map +1 -1
- package/dist/client/locales/pt-BR.d.ts.map +1 -1
- package/dist/client/locales/pt-BR.js +2 -0
- package/dist/client/locales/pt-BR.js.map +1 -1
- package/dist/client/locales/runWait.en-US.d.ts +2 -0
- package/dist/client/locales/runWait.en-US.d.ts.map +1 -1
- package/dist/client/locales/runWait.en-US.js +2 -0
- package/dist/client/locales/runWait.en-US.js.map +1 -1
- package/dist/client/locales/runWait.zh-CN.d.ts.map +1 -1
- package/dist/client/locales/runWait.zh-CN.js +2 -0
- package/dist/client/locales/runWait.zh-CN.js.map +1 -1
- package/dist/client/locales/vi-VN.d.ts.map +1 -1
- package/dist/client/locales/vi-VN.js +2 -0
- package/dist/client/locales/vi-VN.js.map +1 -1
- package/dist/client/locales/zh-TW.d.ts.map +1 -1
- package/dist/client/locales/zh-TW.js +2 -0
- package/dist/client/locales/zh-TW.js.map +1 -1
- package/dist/client/pages/agents/agent-dialog.js +4 -1
- package/dist/client/pages/agents/agent-dialog.js.map +1 -1
- package/dist/client/runs/format-wait.d.ts.map +1 -1
- package/dist/client/runs/format-wait.js +2 -0
- package/dist/client/runs/format-wait.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
- package/dist/server/core/conversations/conversation.service.js +43 -3
- package/dist/server/core/conversations/conversation.service.js.map +1 -1
- package/dist/server/core/conversations/links.d.ts +3 -0
- package/dist/server/core/conversations/links.d.ts.map +1 -1
- package/dist/server/core/conversations/links.js +33 -0
- package/dist/server/core/conversations/links.js.map +1 -1
- package/dist/server/core/conversations/ports.d.ts +1 -1
- package/dist/server/core/conversations/ports.d.ts.map +1 -1
- package/dist/server/core/runs/claim.d.ts.map +1 -1
- package/dist/server/core/runs/claim.js +24 -7
- package/dist/server/core/runs/claim.js.map +1 -1
- package/dist/server/core/runs/run.service.d.ts +1 -1
- package/dist/server/core/runs/run.service.d.ts.map +1 -1
- package/dist/server/core/runs/run.service.js +7 -1
- package/dist/server/core/runs/run.service.js.map +1 -1
- package/dist/server/core/runs/workload.d.ts.map +1 -1
- package/dist/server/core/runs/workload.js +19 -2
- package/dist/server/core/runs/workload.js.map +1 -1
- package/dist/server/kernel/events.d.ts +2 -0
- package/dist/server/kernel/events.d.ts.map +1 -1
- package/dist/server/kernel/events.js.map +1 -1
- package/dist/server/online/agent/runner/backend.d.ts.map +1 -1
- package/dist/server/online/agent/runner/backend.js +17 -8
- package/dist/server/online/agent/runner/backend.js.map +1 -1
- package/dist/server/providers/agents.d.ts.map +1 -1
- package/dist/server/providers/agents.js +3 -0
- package/dist/server/providers/agents.js.map +1 -1
- package/dist/server/routes/schemas.d.ts +1 -1
- package/dist/server/routes/schemas.d.ts.map +1 -1
- package/dist/server/routes/schemas.js +6 -2
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/shared/conversations.d.ts +6 -0
- package/dist/shared/conversations.d.ts.map +1 -1
- package/dist/shared/conversations.js.map +1 -1
- package/dist/shared/runs.d.ts +7 -3
- package/dist/shared/runs.d.ts.map +1 -1
- package/dist/shared/runs.js +2 -0
- package/dist/shared/runs.js.map +1 -1
- package/dist/shared/ui-message.d.ts +2 -2
- package/dist/shared/ui-message.d.ts.map +1 -1
- package/dist/shared/ui-message.js +1 -1
- package/dist/shared/ui-message.js.map +1 -1
- package/package.json +11 -11
- package/skills/nocobase-app-plugin-agents/references/verification-and-troubleshooting.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @nocobase/app-plugin-agents
|
|
2
2
|
|
|
3
|
+
## 2.0.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a54cf2c: Expose the optional target update time in conversation links so applications can order linked work without fetching each record.
|
|
8
|
+
- 09646a9: Continue queued Runner conversation messages in the next turn after a user stops the current turn, keeping the original messages and excluding received or withdrawn inputs.
|
|
9
|
+
|
|
10
|
+
Preserve saved input revisions across stream recovery without allowing older assistant snapshots to overwrite replayed output.
|
|
11
|
+
|
|
12
|
+
- f206179: Explain quota-blocked queued runs and model diagnostics with `toolQuota` and the earliest known reset time, reusing existing quota marks without changing scheduling or retries.
|
|
13
|
+
|
|
14
|
+
## 2.0.0-beta.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 88d35b8: Share conversation snapshot reads, wait for subscription acknowledgement, and avoid redundant agent catalog refreshes.
|
|
19
|
+
- 88d35b8: Reduce idle conversation recovery reads while preserving latest response revisions, missed messages, reconnect recovery and running refreshes.
|
|
20
|
+
- d04061e: Batch linked work in conversation snapshots and share cursor-based message refreshes across chat surfaces. Refresh changed conversation state without repeatedly reading archived lists, and reconcile saved history once after acknowledged reconnection.
|
|
21
|
+
- 88d35b8: Avoid redundant conversation detail and related-data reads after read acknowledgements while preserving cross-tab updates and reconnect recovery.
|
|
22
|
+
- 88d35b8: Allow runs to be filtered by multiple statuses in one paginated request.
|
|
23
|
+
- 36f61d0: Distinguish workspace preparation from Agent startup when reusing a runner workspace.
|
|
24
|
+
|
|
25
|
+
Keep received replies before follow-up messages queued during an active conversation.
|
|
26
|
+
|
|
27
|
+
- cf23bfe: Move Redis realtime broadcasting to the commercial Realtime Redis plugin. Register its Server plugin before selecting Redis in an application, or import the standalone broker from its `/broker` entry. Preserve the Memory default, service identity, authorization and recovery protocol, and republish runtime consumers against the new host and realtime major versions. Install the compatible commercial cohort before deploying the final removal.
|
|
28
|
+
- 88d35b8: Document every allowed run status for single and combined status filters in the API and command reference.
|
|
29
|
+
- b29a336: Keep saved Agent descriptions current when reopening the editor before a list refresh completes, and expose the existing authorization permission topic constants to client integrations.
|
|
30
|
+
- Updated dependencies [88d35b8]
|
|
31
|
+
- Updated dependencies [83f28b1]
|
|
32
|
+
- Updated dependencies [88d35b8]
|
|
33
|
+
- Updated dependencies [88d35b8]
|
|
34
|
+
- Updated dependencies [83f28b1]
|
|
35
|
+
- Updated dependencies [cf23bfe]
|
|
36
|
+
- Updated dependencies [b29a336]
|
|
37
|
+
- @nocobase/app-client@4.0.0-beta.6
|
|
38
|
+
- @nocobase/authorization@1.0.0-beta.16
|
|
39
|
+
- @nocobase/app-plugin-authorization@1.0.0-beta.34
|
|
40
|
+
- @nocobase/app-plugin-authentication@2.0.0-beta.17
|
|
41
|
+
- @nocobase/app-server@4.0.0-beta.0
|
|
42
|
+
- @nocobase/app-cli@1.0.0-beta.31
|
|
43
|
+
- @nocobase/app-plugin-file@1.0.0-beta.28
|
|
44
|
+
|
|
3
45
|
## 2.0.0-beta.0
|
|
4
46
|
|
|
5
47
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Repository access providers that mint credentials in `prepare` implement the ide
|
|
|
28
28
|
|
|
29
29
|
- **Runs** (`agRuns`): one agent working on one subject, keyed by (agent, subject, thread) (`agentType` is `runner`; `online` only on runs earlier versions kept). New work joins the run already on it as the same person (`actorUserId`) while that run can still be told, else that person's queued one, else starts a new run: work woken by different people never shares a run, so it neither borrows another's identity nor waits for a runner that will not take it. Runs of one key are still claimed one at a time, whoever they work as. Queued runs are claimed by `priority` (lower first; 0 by default), then oldest first; a run given a `fireAt` is not claimed before that moment. Work merged into a queued run raises it to the more urgent priority and the earlier moment of the two. A claim hands a run to a runner that fits in one transaction: the agent's tool enabled, installed and signed in there with its model among those the runner reports for the tool and not marked unavailable there (`runsEntry` in `shared/runners.ts`; recorded on the run as `tool` and `model` and passed to the tool; a model the tool's account cannot use fails the run `modelUnavailable` and marks that tool and model unavailable on the runner, `Runner.unavailableModels`, until it reports the tool's models differently), the runner shared with all users or owned by the person who woke the agent, its features covering what the run needs (worked out from the payload: checkouts, directories, skills, secrets), a shared runner when one of the variables the run gets is for shared runners only (`teamRunnersOnly`), and a free slot within the agent's concurrency. The variables are checked and opened in the claim's transaction, so the runner receives what was checked; a personal runner left out for them notes on the run which variables asked for a shared runner (`teamOnlyVariables`, cleared when the run is taken, requeued or given new input), and its wait is `secretsNotAllowed` with those variables. Before a run exists, `eligibility` (`runnersFor`, `canClaim`, `mayQueue`, `teamOnly`) answers by the same rules whether a runner would take an agent's work done as a person, with the scopes and features the caller names; `availability` and `GET /api/agents/available` use it for the caller. A working directory that is a directory on one runner pins the run to that runner, one run at a time per directory. The claim renders the brief (kept per run, `agRunBriefs`), mints a run token, and hands over the working directories, the merged variables, the skills and the CLI to install. Events, usage and branches are recorded as the runner reports them; completing requires every input to be handled. A failed attempt is tried again after 30 seconds (`agents.runs.retryBaseDelayMs`), doubling up to ten minutes, except when the runner went away or lost its lease, which retries at once. A runner that finds the subject's working directory held by another live run reports `workspaceBusy`: the run goes back to the queue, freeing its slot, and is tried again after 30 seconds, doubling up to two minutes; these waits do not count against the agent's attempts, and the run fails `workspaceBusy` only once it has waited six hours (`WORKSPACE_WAIT_LIMIT_MS`).
|
|
30
30
|
- **Run requests** (`agRunRequests`): work someone asked of an agent on a subject another person answers for, waiting for that person to confirm it (see "Work someone else causes" below).
|
|
31
|
-
- **Workload** (`runs.workload({ subjectKind, limit? })`, `shared/runs.ts`): the open runs on one kind of subject for a board of work, without claiming anything. Held runs come with their runner, the newest input's trigger and their newest text, tool call, status or error (`lastActivity`); queued runs with their place in the claim order (among the runs claimable now, and among the agent's) and why they wait, by the claim's own rules in its order (`RUN_WAIT_REASONS`: the agent not enabled, `agentArchived`, delayed until a moment, no runner online, the agent's named runners offline, its tool not signed in anywhere, only other people's personal runners, missing features, only personal runners whose owner may not receive the run's variables, the same work already held, the agent's concurrency full, every fitting runner busy, preparation retrying, or next). A wait is a code and the values its words need (`params`), never text: the client words it with `formatRunWait(t, wait)` from `client/runs.ts`, in this plugin's namespace (`runWait.reasons.<reason>`), which an application may override in its locale file or replace with its own formatter. It also answers each agent's load (held, queued, its limit, whether a runner could take its work) and the runners' (online, holding work, slots and slots used). It reads a bounded number of tables, never one query per subject; the caller decides what of it a person may see.
|
|
31
|
+
- **Workload** (`runs.workload({ subjectKind, limit? })`, `shared/runs.ts`): the open runs on one kind of subject for a board of work, without claiming anything. Held runs come with their runner, the newest input's trigger and their newest text, tool call, status or error (`lastActivity`); queued runs with their place in the claim order (among the runs claimable now, and among the agent's) and why they wait, by the claim's own rules in its order (`RUN_WAIT_REASONS`: the agent not enabled, `agentArchived`, delayed until a moment, no runner online, the agent's named runners offline, its tool quota exhausted (`toolQuota`, with the earliest known reset in `until` and `params.until`, or null/absent when unknown), its tool not signed in anywhere, only other people's personal runners, missing features, only personal runners whose owner may not receive the run's variables, the same work already held, the agent's concurrency full, every fitting runner busy, preparation retrying, or next). A wait is a code and the values its words need (`params`), never text: the client words it with `formatRunWait(t, wait)` from `client/runs.ts`, in this plugin's namespace (`runWait.reasons.<reason>`), which an application may override in its locale file or replace with its own formatter. It also answers each agent's load (held, queued, its limit, whether a runner could take its work) and the runners' (online, holding work, slots and slots used). It reads a bounded number of tables, never one query per subject; the caller decides what of it a person may see.
|
|
32
32
|
- **Redaction**: what runners report is stored redacted. The runner redacts first; the server redacts every run event (content, output, input and meta), summary and failure detail, and every job event and failure detail again before storing them, with the protocol's redactor (`createRedactor` of `@nocobase/agent-protocol`): the secrets the claim handed out (a run's variables and token, a job's opened variables, repository token and upload headers, remembered in memory for recent claims) and the common secret patterns, each replaced with `[REDACTED]` (`server/kernel/redaction.ts`). After a restart, or on another instance, the patterns and the runner's own redaction still apply.
|
|
33
33
|
- **The sweep** (`server/runners/sweeper.ts`, the `RunnersSweep` schedule every 30 seconds, or `agents.runners.sweepIntervalMs`, on the application's jobs service, or an in-process timer without one) marks runners not heard from for 150 s offline, takes jobs back from silent runners and fails jobs far past their timeout, and takes runs back from runners that went away, lost their lease or never started, fails runs idle for twice their idle timeout, records unacknowledged cancellations, cancels queued runs of archived agents, and fails a queued run that waited longer than its subject allows (`queuedExpiryMs` on the subject's binding, counted from the run's last change or from its `fireAt`) as `queuedExpired`.
|
|
34
34
|
- **Variables** (`agSecrets`): environment variables for the agent's process, every one a secret sealed with the application's secrets service (`secrets.keys`) and bound to its scope and name, set on an agent, a working directory (`workdir`) or a scope the application registers, merged for a run in the order the subject names its scopes < its working directories < the agent, delivered only to runners with the `secrets` feature: to whichever runner takes the run, a personal runner of anyone who may use the agent included, since sharing an agent shares the tokens it works with. A variable marked `teamRunnersOnly` (off by default; `PUT /api/agents/variables/...` with `teamRunnersOnly`, and without `value` to change only that) keeps runs and jobs that get it on shared runners. Jobs check the exact variables referenced by their prepared spec, including repository credentials, before opening any value in the claim transaction; a personal runner skips such a job without counting a preparation failure, leaving it queued for a shared runner and continuing to other jobs. Values are write-only; reading them back and every delivery are audited (`agSecretAudits`).
|
|
@@ -330,6 +330,12 @@ Studio publishes `agents.runs.retentionDays` through its existing public configu
|
|
|
330
330
|
|
|
331
331
|
## Conversations while the agent works
|
|
332
332
|
|
|
333
|
+
Conversation list and detail snapshots include `links`, resolved for the conversation owner. A list page reads link rows together and calls each registered kind's resolver once per owner with distinct target IDs; resolvers should keep that batch intact. The standalone links endpoint remains available for command-line clients.
|
|
334
|
+
|
|
335
|
+
Dependent projections can use `useConversation(id, { passive: true })` to observe the genuine detail query without another mount/focus refresh or polling timer. Initial missing data and explicit invalidations still use the same real reader and shared in-flight request.
|
|
336
|
+
|
|
337
|
+
`ChatProvider` owns realtime refreshes across mounted surfaces. Message announcements use their durable `lastSeq` cursor and share the controller's in-flight `after` read; only an uncovered cursor gap needs another page. Revision announcements reread the affected message. State changes refresh the affected detail and its dependent queries, while archived lists wait until opened (`useConversationList(query, enabled)`). Read receipts update cached list positions without another list request. The existing five-second running fallback remains; acknowledged reconnects reconcile the loaded history window, including revisions and removed messages, before resuming a stream.
|
|
338
|
+
|
|
333
339
|
A Runner conversation takes a message while its agent works: `POST /api/agents/conversations/{conversationId}/queue` (`OnlineAgent.queue`) joins it to the run at work, steered into the running turn where the coding tool can take it, else taken as the next turn; `POST …/messages/{messageId}/withdraw` takes it back until a runner has it (`400 CONVERSATION_CONFLICT`, `messageTaken`, after). Delivery and withdrawal are conditional updates of the input row on an empty `deliveredAt` and `handledAt`, so a message is never both withdrawn and delivered. `POST …/markSeen { seq }` moves the owner's read position, `GET /api/agents/conversationAttention` answers the badge counts, and `GET …/usage` sums tokens, cost and the latest context window over the conversation's runs.
|
|
334
340
|
|
|
335
341
|
A run can ask its conversation owner with `conversation ask-owner "Proceed with the plan?" --issue PM-721`. The optional `issue` identifier is saved with the question and its server timestamp in conversation metadata. Without it, behavior is unchanged. Applications can call `conversations.settleOwnerQuestion(id, issue, resumedAt)` with the actual task transition timestamp to settle only a matching earlier question; it emits `conversation.changed` without changing read positions, unread replies or waking a run. The application owns task visibility, links and resumption rules.
|
|
@@ -379,3 +385,5 @@ The runtime status cell displays reported capacity, a pending-sync notice when o
|
|
|
379
385
|
`POST /api/agents/conversations/{conversationId}/switchAgent { agentId }` changes an idle Runner conversation to another enabled, callable Runner Agent. Its history and links stay; the next message starts a fresh tool session. Pending, running and finalizing work must finish first. Switching does not cancel a run or send a message.
|
|
380
386
|
|
|
381
387
|
To defer a selection until a message is accepted, pass optional `agentId` alongside the current SDK user `message` to `POST /api/agents/conversations/{conversationId}/messages`. Only idle, unarchived Runner conversations can change agents this way. Admission validates the target and commits the new binding, fresh thread, one switch notice, message and target run in the existing transaction. Selecting the current agent adds no notice; omitting the field preserves existing behavior. Rejection rolls everything back. A later execution failure keeps the accepted switch; an interrupted response must be reconciled through history, never automatically resent. Queued input and native conversations do not accept this field.
|
|
388
|
+
|
|
389
|
+
Quota diagnostics (`claims.selectionWait` / `claims.diagnose`) use `toolQuota` for an otherwise fitting tool/model blocked by existing quota marks. Each model entry optionally carries `until`: the earliest known reset across its eligible runners, after all exhausted windows on that runner reset. An unknown reset is null. Non-quota tool failures keep `toolUnavailable`; another usable candidate keeps its existing diagnosis. These explanations do not change dispatch, retries or stored quota marks.
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import type { QueryClient, QueryKey } from '@tanstack/react-query';
|
|
2
|
-
/** How long announcements gather before the queries they name are read again. */
|
|
3
2
|
export declare const CHAT_INVALIDATION_MS = 150;
|
|
4
3
|
interface CoalescedInvalidation {
|
|
5
|
-
|
|
6
|
-
readonly add: (queryKey: QueryKey) => void;
|
|
4
|
+
readonly add: (key: QueryKey, exact?: boolean) => void;
|
|
7
5
|
readonly stop: () => void;
|
|
8
6
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
* marking the same lists and details stale would read them again every time. Gathered for `delay`, they are read once.
|
|
12
|
-
*/
|
|
13
|
-
export declare function coalescedInvalidation(client: Pick<QueryClient, 'invalidateQueries'>, delay?: number): CoalescedInvalidation;
|
|
7
|
+
/** Coalesce bursts and retain a trailing read for changes received during a snapshot. */
|
|
8
|
+
export declare function coalescedInvalidation(client: QueryClient, delay?: number): CoalescedInvalidation;
|
|
14
9
|
export {};
|
|
15
10
|
//# sourceMappingURL=coalesced-invalidation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coalesced-invalidation.d.ts","sourceRoot":"","sources":["../../../client/chat/coalesced-invalidation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"coalesced-invalidation.d.ts","sourceRoot":"","sources":["../../../client/chat/coalesced-invalidation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEnE,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,UAAU,qBAAqB;IAC7B,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,yFAAyF;AACzF,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,EACnB,KAAK,GAAE,MAA6B,GACnC,qBAAqB,CAgCvB"}
|
|
@@ -1,29 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
import { createRealtimeRefresh, refreshRealtimeQueries, } from '@nocobase/app-client';
|
|
2
2
|
export const CHAT_INVALIDATION_MS = 150;
|
|
3
|
-
/**
|
|
4
|
-
* A run announces its conversation many times in a moment (the message, its state, its title); each announcement
|
|
5
|
-
* marking the same lists and details stale would read them again every time. Gathered for `delay`, they are read once.
|
|
6
|
-
*/
|
|
3
|
+
/** Coalesce bursts and retain a trailing read for changes received during a snapshot. */
|
|
7
4
|
export function coalescedInvalidation(client, delay = CHAT_INVALIDATION_MS) {
|
|
8
5
|
const keys = new Map();
|
|
9
|
-
|
|
6
|
+
const scheduled = createRealtimeRefresh(async (signal) => {
|
|
7
|
+
const due = [...keys.values()];
|
|
8
|
+
keys.clear();
|
|
9
|
+
await Promise.all(due
|
|
10
|
+
.filter(({ queryKey: key, exact }) => !due.some((other) => !other.exact &&
|
|
11
|
+
(other.queryKey.length < key.length ||
|
|
12
|
+
(exact && other.queryKey.length === key.length)) &&
|
|
13
|
+
other.queryKey.every((part, index) => part === key[index])))
|
|
14
|
+
.map(({ queryKey, exact }) => refreshRealtimeQueries(client, { queryKey, exact }, signal)));
|
|
15
|
+
}, { delayMs: delay });
|
|
10
16
|
return {
|
|
11
|
-
add(queryKey) {
|
|
12
|
-
keys.set(JSON.stringify(queryKey), queryKey);
|
|
13
|
-
|
|
14
|
-
timer = undefined;
|
|
15
|
-
const due = [...keys.values()];
|
|
16
|
-
keys.clear();
|
|
17
|
-
for (const key of due)
|
|
18
|
-
void client.invalidateQueries({ queryKey: key });
|
|
19
|
-
}, delay);
|
|
20
|
-
},
|
|
21
|
-
stop() {
|
|
22
|
-
if (timer !== undefined)
|
|
23
|
-
clearTimeout(timer);
|
|
24
|
-
timer = undefined;
|
|
25
|
-
keys.clear();
|
|
17
|
+
add(queryKey, exact = false) {
|
|
18
|
+
keys.set(JSON.stringify([queryKey, exact]), { queryKey, exact });
|
|
19
|
+
scheduled.request();
|
|
26
20
|
},
|
|
21
|
+
stop: scheduled.close,
|
|
27
22
|
};
|
|
28
23
|
}
|
|
29
24
|
//# sourceMappingURL=coalesced-invalidation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coalesced-invalidation.js","sourceRoot":"","sources":["../../../client/chat/coalesced-invalidation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"coalesced-invalidation.js","sourceRoot":"","sources":["../../../client/chat/coalesced-invalidation.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAG9B,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAOxC,yFAAyF;AACzF,MAAM,UAAU,qBAAqB,CACnC,MAAmB,EACnB,QAAgB,oBAAoB;IAEpC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkD,CAAC;IACvE,MAAM,SAAS,GAAG,qBAAqB,CACrC,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,OAAO,CAAC,GAAG,CACf,GAAG;aACA,MAAM,CACL,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,CAC3B,CAAC,GAAG,CAAC,IAAI,CACP,CAAC,KAAK,EAAE,EAAE,CACR,CAAC,KAAK,CAAC,KAAK;YACZ,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM;gBACjC,CAAC,KAAK,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC;YAClD,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAC7D,CACJ;aACA,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,CAC3B,sBAAsB,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,MAAM,CAAC,CAC5D,CACJ,CAAC;IACJ,CAAC,EACD,EAAE,OAAO,EAAE,KAAK,EAAE,CACnB,CAAC;IACF,OAAO;QACL,GAAG,CAAC,QAAkB,EAAE,KAAK,GAAG,KAAK;YACnC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YACjE,SAAS,CAAC,OAAO,EAAE,CAAC;QACtB,CAAC;QACD,IAAI,EAAE,SAAS,CAAC,KAAK;KACtB,CAAC;AACJ,CAAC"}
|
|
@@ -10,7 +10,7 @@ type HistoryReplacement = Extract<ConversationChanged, {
|
|
|
10
10
|
}>;
|
|
11
11
|
import { Chat } from '@ai-sdk/react';
|
|
12
12
|
import { type ReactElement, type ReactNode } from 'react';
|
|
13
|
-
import type
|
|
13
|
+
import { type ConversationDetail } from '../../shared/conversations.js';
|
|
14
14
|
import type { AppUIMessage, AppUIToolApprovalInput } from '../../shared/online-agent.js';
|
|
15
15
|
import { ChatApi } from './api.js';
|
|
16
16
|
import { ChatSubmissions, type SubmissionSend } from './submissions.js';
|
|
@@ -24,14 +24,26 @@ export type StreamState = {
|
|
|
24
24
|
/** Application coordination around one SDK Chat per conversation, shared by every surface. */
|
|
25
25
|
export declare class ChatController {
|
|
26
26
|
readonly api: ChatApi;
|
|
27
|
+
private invalidations?;
|
|
28
|
+
invalidateQuery(key: readonly unknown[], exact?: boolean): void;
|
|
29
|
+
invalidateLists(): void;
|
|
30
|
+
/** Reuse a detail snapshot when replacing its row cannot change membership, order or a page cursor. */
|
|
31
|
+
private syncLists;
|
|
27
32
|
private historyEpochs;
|
|
33
|
+
private messageEpochs;
|
|
28
34
|
private replacements;
|
|
29
35
|
private resendRequests;
|
|
36
|
+
private streamFollowers;
|
|
37
|
+
private detailReads;
|
|
38
|
+
private refreshes;
|
|
30
39
|
private reconciles;
|
|
31
40
|
readonly submissions: ChatSubmissions;
|
|
32
41
|
private chats;
|
|
33
42
|
private histories;
|
|
34
43
|
private loads;
|
|
44
|
+
private messageReads;
|
|
45
|
+
private catchups;
|
|
46
|
+
private reconnects;
|
|
35
47
|
private refreshAgain;
|
|
36
48
|
private recoveries;
|
|
37
49
|
private admissions;
|
|
@@ -48,6 +60,7 @@ export declare class ChatController {
|
|
|
48
60
|
private generation;
|
|
49
61
|
private detailVersions;
|
|
50
62
|
private receiptVersions;
|
|
63
|
+
private pendingReceipts;
|
|
51
64
|
private commandVersions;
|
|
52
65
|
constructor(api: ChatApi);
|
|
53
66
|
chat(id: string): Chat<AppUIMessage>;
|
|
@@ -61,6 +74,10 @@ export declare class ChatController {
|
|
|
61
74
|
private historyMeta;
|
|
62
75
|
dispatch(id: string, action: MessagesAction): void;
|
|
63
76
|
private historyLifetime;
|
|
77
|
+
private messageLifetime;
|
|
78
|
+
private readLifetime;
|
|
79
|
+
/** A resumed stream replaces replay snapshots, not already requested durable message revisions. */
|
|
80
|
+
private invalidateReplayReads;
|
|
64
81
|
private invalidateHistoryReads;
|
|
65
82
|
historyReplaced(event: HistoryReplacement): void;
|
|
66
83
|
private replaceHistory;
|
|
@@ -73,6 +90,9 @@ export declare class ChatController {
|
|
|
73
90
|
private reconcile;
|
|
74
91
|
loadHistory(id: string, older?: boolean, fresh?: boolean): Promise<void>;
|
|
75
92
|
reread(id: string, ids: readonly string[]): Promise<AppUIMessage[]>;
|
|
93
|
+
/** A cursor announcement shares the current read; only an actual gap needs a trailing page. */
|
|
94
|
+
messagesChanged(id: string, lastSeq: number): Promise<void>;
|
|
95
|
+
isWatching(id: string): boolean;
|
|
76
96
|
watch(id: string): () => void;
|
|
77
97
|
reconnect(): void;
|
|
78
98
|
messageUpdated(id: string, messageId: string, revision: number): void;
|
|
@@ -86,19 +106,26 @@ export declare class ChatController {
|
|
|
86
106
|
private sendInput;
|
|
87
107
|
state(id: string): StreamState | undefined;
|
|
88
108
|
private recoveryKey;
|
|
109
|
+
followStream(id: string): () => void;
|
|
89
110
|
receive(id: string, known?: ConversationDetail): void;
|
|
90
111
|
resume(id: string, known?: ConversationDetail): Promise<void>;
|
|
91
112
|
approve(id: string, input: AppUIToolApprovalInput): Promise<void>;
|
|
92
113
|
finalize(id: string): Promise<void>;
|
|
93
|
-
|
|
114
|
+
/** One detail request shared by the query observer and message reconciliation. */
|
|
115
|
+
readDetail(id: string, fresh?: boolean): Promise<ConversationDetail>;
|
|
116
|
+
refresh(id: string, fresh?: boolean, idleProbe?: boolean, reconcileHistory?: boolean): Promise<void>;
|
|
117
|
+
private refreshOnce;
|
|
94
118
|
private scheduleRefresh;
|
|
95
|
-
observeDetail(detail: ConversationDetail, refresh?: boolean): void;
|
|
119
|
+
observeDetail(detail: ConversationDetail, refresh?: boolean, synchronizeLists?: boolean): void;
|
|
96
120
|
isStopping(id: string): boolean;
|
|
97
121
|
stop(id: string): Promise<ConversationDetail>;
|
|
98
122
|
lifetime(): () => boolean;
|
|
99
123
|
private nextDetailVersion;
|
|
100
124
|
detailRequest(id: string): () => boolean;
|
|
101
125
|
private remember;
|
|
126
|
+
/** The HTTP receipt supplies this tab's snapshot; retain a newer read position for a trailing read. */
|
|
127
|
+
observeReceipt(id: string, seenSeq: number): void;
|
|
128
|
+
deferReadReceipt(id: string, seenSeq: number, refresh: () => void): boolean;
|
|
102
129
|
markRead(id: string): Promise<ConversationDetail>;
|
|
103
130
|
command<T>(id: string, request: () => Promise<T>, detail: (result: T) => ConversationDetail): Promise<T>;
|
|
104
131
|
subscribe: (callback: () => void) => (() => void);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../client/chat/controller.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../client/chat/controller.tsx"],"names":[],"mappings":"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"}
|