nexus-ai-pro 2.8.0 → 2.9.0
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/API_STABILITY.md +34 -0
- package/CHANGELOG.md +109 -1
- package/README.md +15 -4
- package/dist/batch/poll.d.ts +8 -4
- package/dist/batch/poll.js +2 -1
- package/dist/deep-agent/helpers.d.ts +4 -0
- package/dist/deep-agent/helpers.js +56 -4
- package/dist/evaluate/online.d.ts +5 -0
- package/dist/evaluate/online.js +74 -5
- package/dist/lifecycle/budget.d.ts +60 -3
- package/dist/lifecycle/budget.js +147 -3
- package/dist/lifecycle/index.d.ts +2 -2
- package/dist/lifecycle/reservations.d.ts +46 -0
- package/dist/lifecycle/reservations.js +67 -0
- package/dist/media/rag.d.ts +76 -1
- package/dist/media/rag.js +195 -53
- package/dist/postgres/tenancy.d.ts +26 -1
- package/dist/postgres/tenancy.js +116 -1
- package/dist/postgres/traces.d.ts +2 -1
- package/dist/postgres/traces.js +13 -1
- package/dist/server/tenancy.d.ts +33 -2
- package/dist/server/tenancy.js +162 -0
- package/dist/tracing/index.d.ts +2 -2
- package/dist/tracing/index.js +1 -1
- package/dist/tracing/query.d.ts +5 -1
- package/dist/tracing/query.js +14 -1
- package/dist/tracing/stores.d.ts +2 -2
- package/dist/tracing/stores.js +2 -2
- package/dist/types/lifecycle.d.ts +92 -1
- package/dist/types/tracing.d.ts +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/video/errors.d.ts +37 -0
- package/dist/video/errors.js +22 -0
- package/dist/video/google.d.ts +52 -0
- package/dist/video/google.js +248 -0
- package/dist/video/index.d.ts +426 -0
- package/dist/video/index.js +666 -0
- package/dist/video/mock.d.ts +62 -0
- package/dist/video/mock.js +97 -0
- package/dist/video/runway.d.ts +47 -0
- package/dist/video/runway.js +241 -0
- package/dist-cjs/batch/poll.d.ts +8 -4
- package/dist-cjs/batch/poll.js +2 -1
- package/dist-cjs/deep-agent/helpers.d.ts +4 -0
- package/dist-cjs/deep-agent/helpers.js +56 -4
- package/dist-cjs/evaluate/online.d.ts +5 -0
- package/dist-cjs/evaluate/online.js +74 -5
- package/dist-cjs/lifecycle/budget.d.ts +60 -3
- package/dist-cjs/lifecycle/budget.js +147 -3
- package/dist-cjs/lifecycle/index.d.ts +2 -2
- package/dist-cjs/lifecycle/reservations.d.ts +46 -0
- package/dist-cjs/lifecycle/reservations.js +71 -0
- package/dist-cjs/media/rag.d.ts +76 -1
- package/dist-cjs/media/rag.js +195 -53
- package/dist-cjs/postgres/tenancy.d.ts +26 -1
- package/dist-cjs/postgres/tenancy.js +115 -0
- package/dist-cjs/postgres/traces.d.ts +2 -1
- package/dist-cjs/postgres/traces.js +13 -1
- package/dist-cjs/server/tenancy.d.ts +33 -2
- package/dist-cjs/server/tenancy.js +162 -0
- package/dist-cjs/tracing/index.d.ts +2 -2
- package/dist-cjs/tracing/index.js +3 -1
- package/dist-cjs/tracing/query.d.ts +5 -1
- package/dist-cjs/tracing/query.js +16 -1
- package/dist-cjs/tracing/stores.d.ts +2 -2
- package/dist-cjs/tracing/stores.js +3 -1
- package/dist-cjs/types/lifecycle.d.ts +92 -1
- package/dist-cjs/types/tracing.d.ts +20 -1
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/dist-cjs/video/errors.d.ts +37 -0
- package/dist-cjs/video/errors.js +27 -0
- package/dist-cjs/video/google.d.ts +52 -0
- package/dist-cjs/video/google.js +252 -0
- package/dist-cjs/video/index.d.ts +426 -0
- package/dist-cjs/video/index.js +708 -0
- package/dist-cjs/video/mock.d.ts +62 -0
- package/dist-cjs/video/mock.js +101 -0
- package/dist-cjs/video/runway.d.ts +47 -0
- package/dist-cjs/video/runway.js +245 -0
- package/llms.txt +2 -1
- package/package.json +54 -1
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,40 @@ Both are covered by this policy:
|
|
|
41
41
|
Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
|
|
42
42
|
one of its explicit subpaths.
|
|
43
43
|
|
|
44
|
+
## 2.9 stage (2.9.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.9 adds is stable from the start and additive, unless this section says otherwise.
|
|
47
|
+
|
|
48
|
+
New and stable:
|
|
49
|
+
- `OperationFamily` gains `video`;
|
|
50
|
+
- `nexus-ai-pro/video`, `/video/google`, `/video/runway`, and `/video/mock` are new and experimental:
|
|
51
|
+
video generation graduates by the checklist once Veo and Runway pass its conformance suite live;
|
|
52
|
+
- `RunQuery.after` with `RunCursor`, and `runCursor()` and `isAfterCursor()` on
|
|
53
|
+
`nexus-ai-pro/tracing`. A store of your own that does not honour `after` still works with the
|
|
54
|
+
online-evaluation worker, which falls back to offsets;
|
|
55
|
+
- `OnlineSchedulePass.paging`;
|
|
56
|
+
- on `nexus-ai-pro/lifecycle`: `BudgetReservation.id`; `BudgetReservationStatus`,
|
|
57
|
+
`BudgetReservationRecord`, `BudgetReservationOutcome`, `BudgetRecoveryReport`, and
|
|
58
|
+
`BudgetRecoverOptions`; `BudgetLedger.renew()`, `settle()`, and `recover()`, all optional;
|
|
59
|
+
`BudgetLedgerOptions.leaseMs` and `uncertain`; `BudgetLedgerWithReport.reservation()`; and the five
|
|
60
|
+
optional reservation methods of `BudgetUsageStore` with `BudgetReservationChange`. The statuses,
|
|
61
|
+
and which outcome moves a reservation from which status, are part of the promise;
|
|
62
|
+
- `hold()`, `settle()`, `renew()`, `lapsed()`, and `reservation()` on `MemoryTenantUsage`,
|
|
63
|
+
`RedisTenantUsage`, and `PostgresTenantUsage`, and the tenancy migration version 2 with its
|
|
64
|
+
reservations table;
|
|
65
|
+
- on `nexus-ai-pro/media/rag`, experimental with the media family: `MediaIndexOptions.manifest` and
|
|
66
|
+
`name`, `MediaIndexEntry`, `MediaIndexReader`, `MediaIndexReport.unchanged`, and
|
|
67
|
+
`MediaIndex.delete()`, `reindex()`, `describe()`, and `list()`.
|
|
68
|
+
|
|
69
|
+
Changed, each to keep a promise the surface already made:
|
|
70
|
+
- `TraceStore.query()` orders runs that share a start time by id, from last to first, as code units,
|
|
71
|
+
so a page means the same on every store. Before, that order was not defined.
|
|
72
|
+
- `MemoryBudgetUsage`, the ledger's default store, records reservations, so `reserve()` returns one
|
|
73
|
+
with an `id`. An online-evaluation worker reserves every run of its evaluators, a run after a crash
|
|
74
|
+
included, where before it reserved only a first.
|
|
75
|
+
- A deep agent with a checkpointer writes the helpers it creates to `<thread>/helpers-created`, so
|
|
76
|
+
`createHelpers.max` holds for a thread across processes, as it says.
|
|
77
|
+
|
|
44
78
|
## 2.8 stage (2.8.0)
|
|
45
79
|
|
|
46
80
|
Everything 2.8 adds is stable from the start and additive, unless this section says otherwise.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,113 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.9.0] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
Video, and everything shipped, proven.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Video generation.** `nexus-ai-pro/video`: `VideoManager` makes videos from a prompt, a first or
|
|
14
|
+
last frame, or reference images, through a provider's asynchronous job. `generate()` is a durable
|
|
15
|
+
operation that records the job the moment it exists and each video as it is stored, so
|
|
16
|
+
`recover()` in another worker collects a job whose process died, never submitting it twice, and
|
|
17
|
+
streams each video into an asset store. Each generation runs in the lifecycle's new `video`
|
|
18
|
+
family, and is priced per second by `priceVideo()` from `VIDEO_PRICES`, the Veo 3.1 list prices,
|
|
19
|
+
or the account's own. `start()`, `resume()`, `status()`, `download()`, and `cancel()` work from a
|
|
20
|
+
JSON `VideoJobRef` in any process. A request is checked against the model's capabilities before
|
|
21
|
+
any job, and a failed job is submitted again only when the provider says that may help.
|
|
22
|
+
- `GoogleVideoProvider` (`/video/google`): Veo through its long-running predictions, on the
|
|
23
|
+
Gemini API, the video read from Google's URI with the key, or on Vertex AI, read from Cloud
|
|
24
|
+
Storage or inline.
|
|
25
|
+
- `RunwayVideoProvider` (`/video/runway`): Runway's task API, for its own models and the hosted
|
|
26
|
+
ones it serves, Veo, Gemini Omni Flash, and Seedance among them; each video read from its signed
|
|
27
|
+
URL without the secret.
|
|
28
|
+
- `MockVideoProvider` (`/video/mock`), and `runVideoConformance()`, which all three pass.
|
|
29
|
+
|
|
30
|
+
New guide `docs/video.md`. The family is experimental until Veo and Runway pass the conformance
|
|
31
|
+
suite live. OpenAI's Videos API was removed on 2026-09-24, so there is no OpenAI adapter.
|
|
32
|
+
- **The experimental families, live.** `npm run test:families:live`, and a weekly *Family
|
|
33
|
+
conformance* job beside the vector stores: media understanding's conformance suite against an
|
|
34
|
+
OpenAI or Gemini vision model and Mistral's OCR, on a PDF and a PNG the script builds; a two-input
|
|
35
|
+
batch through the Gemini embedding batch tier, waited on for 20 minutes and cancelled past that;
|
|
36
|
+
and, when the repository variable `NEXUS_LIVE_VIDEO` is set, the video conformance suite on Veo 3.1
|
|
37
|
+
Lite and Runway. Each check runs where its key is set, and what it measured is kept. These are the
|
|
38
|
+
runs media, the Gemini batch tier, and video graduate on.
|
|
39
|
+
- **Paging by cursor.** `RunQuery.after` takes a `RunCursor`, the start time and id of a page's last
|
|
40
|
+
run, and returns only the runs after it; `runCursor()` takes the cursor from a run, and
|
|
41
|
+
`isAfterCursor()` is the test on its own, for a trace store of your own. Every bundled store
|
|
42
|
+
honours it, and `applyQuery()` does for a store that filters in memory. A new Postgres trace store
|
|
43
|
+
migration, version 3, indexes runs in query order.
|
|
44
|
+
- **Budget reservations that outlive their process.** Over a store that records reservations, a
|
|
45
|
+
`budgetLedger()` holds each one as a `BudgetReservationRecord` under a lease (`leaseMs`, 10
|
|
46
|
+
minutes), and moves it with its total in one step: `reserved`, then `settled`, `released`,
|
|
47
|
+
`expired`, or `uncertain`. `recover(resolve)` settles reservations whose lease lapsed by what
|
|
48
|
+
became of each operation, a `BudgetReservationOutcome`; `settle(id, outcome)` does it for one from
|
|
49
|
+
any process; `renew()` extends a lease; and `uncertain: 'charge' | 'release'` says what an unknown
|
|
50
|
+
outcome costs. `MemoryBudgetUsage`, `MemoryTenantUsage`, `RedisTenantUsage`, and
|
|
51
|
+
`PostgresTenantUsage` all record them: Redis keeps a reservation in its total's Cluster slot, so one
|
|
52
|
+
script moves both, and Postgres does it in one statement, in the reservations table of a new
|
|
53
|
+
tenancy migration, version 2. A store of your own records them through five optional methods on
|
|
54
|
+
`BudgetUsageStore`.
|
|
55
|
+
- **A media index that remembers what it holds.** `MediaIndexOptions.manifest`, any `Store`, with a
|
|
56
|
+
`name`, keeps one `MediaIndexEntry` per medium: a hash of its content, source, metadata, and
|
|
57
|
+
indexing options, where it is kept, its pages, its chunk ids, the embedding model, and the
|
|
58
|
+
`MediaIndexReader` that read and described it. `add()` then skips a medium at the same version and
|
|
59
|
+
replaces exactly the chunks and asset of one that changed, and `MediaIndex` gains `delete()`,
|
|
60
|
+
`reindex()`, `describe()`, and `list()`. `MediaIndexReport.unchanged` counts what was skipped. A
|
|
61
|
+
change is recorded as pending first, so a crash leaves nothing the next call cannot finish.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- Runs that started in the same millisecond come by id, from last to first, compared as code units,
|
|
66
|
+
on every trace store. The order of such runs was not defined before, and the memory and Postgres
|
|
67
|
+
stores disagreed on it.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- An online-evaluation worker reads a backlog of runs that started in the same millisecond through,
|
|
72
|
+
however many pages it spans. Before, it resumed each page at the last run's start time, so a page
|
|
73
|
+
that ended among such runs was read again on every pass, and `drain()` never finished. On a trace
|
|
74
|
+
store of your own that does not honour `RunQuery.after`, the worker pages by offset instead, and
|
|
75
|
+
its `OnlineSchedulePass` says `paging: 'offset'`.
|
|
76
|
+
- A budget reservation held by an online-evaluation worker that dies no longer stays charged for the
|
|
77
|
+
period. The worker records the reservation just before it calls its evaluators, and whoever takes
|
|
78
|
+
over settles it: charged at what the scores cost if they were written, given back if the
|
|
79
|
+
evaluators never ran, and by the ledger's `uncertain` policy if they ran and wrote nothing.
|
|
80
|
+
- An evaluation run again after a crash is reserved and charged. Its evaluators run again to find
|
|
81
|
+
the scores still missing, and before, only a first run drew on the budget.
|
|
82
|
+
- A deep agent resumed after a crash counts the helpers it created before. With a checkpointer, each
|
|
83
|
+
created helper is recorded, with its instructions, on `<thread>/helpers-created` before the call
|
|
84
|
+
that created it returns, so the resumed agent cannot pass `createHelpers.max` and delegates to them
|
|
85
|
+
again by name. Before, the count was kept by the process, and a resumed agent could create `max`
|
|
86
|
+
more and no longer found the ones it had made.
|
|
87
|
+
|
|
88
|
+
### Proof
|
|
89
|
+
|
|
90
|
+
- `tests/video.test.ts`: the mock, Veo on the Gemini API and Vertex AI, and Runway pass the video
|
|
91
|
+
conformance suite against stand-ins for their APIs. A request a model cannot make is refused
|
|
92
|
+
before any job. A generation runs through the lifecycle, streams three megabytes into a
|
|
93
|
+
filesystem store, and is priced per second. A generation whose process dies after submitting, or
|
|
94
|
+
after storing its video, is collected by another worker with one submission and one stored video.
|
|
95
|
+
If the recorded job is ignored, the job is submitted twice.
|
|
96
|
+
- `tests/online-evaluation-paging.test.ts`: the memory, JSONL, and Postgres trace stores order 5,000
|
|
97
|
+
runs, 4,000 of them in one millisecond, identically, and page through them by cursor without a
|
|
98
|
+
repeat or a gap. Ten workers reading 1,000 a page schedule each of them exactly once on every
|
|
99
|
+
store, and on a store that ignores the cursor. The previous worker never reads past the burst.
|
|
100
|
+
- `tests/budget-reservations.test.ts`: on the memory, tenant memory, Redis, and Postgres stores a
|
|
101
|
+
reservation moves with its total, is settled from another process by its operation's outcome, and
|
|
102
|
+
of two processes settling it, one changes the total. An online-evaluation worker killed while
|
|
103
|
+
holding a reservation, before its evaluator, during it, or after its scores, leaves the budget at
|
|
104
|
+
what the judge billed once another worker takes over. With recovery switched off, the same test
|
|
105
|
+
finds the dead worker's estimate still charged.
|
|
106
|
+
- `tests/media-rag.test.ts`: with a manifest, an unchanged medium is neither read nor written, a
|
|
107
|
+
changed one keeps exactly its chunks and one asset, and a deleted one leaves none, with a crash at
|
|
108
|
+
each write of a first index, a change, and a delete. Without the pending record, the crash proof
|
|
109
|
+
fails.
|
|
110
|
+
- `tests/deep-agent-helpers.test.ts`: an agent that created its last allowed helper, killed and
|
|
111
|
+
continued in another process, is refused a second and delegates to the first again by name. Without
|
|
112
|
+
the record read back, the second is created.
|
|
113
|
+
|
|
7
114
|
## [2.8.0] - 2026-10-14
|
|
8
115
|
|
|
9
116
|
Retrieval at production scale, and media that is understood.
|
|
@@ -2452,7 +2559,8 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
2452
2559
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
2453
2560
|
- CLI and audit findings no longer disclose detected secret values.
|
|
2454
2561
|
|
|
2455
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
2562
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.9.0...HEAD
|
|
2563
|
+
[2.9.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.8.0...v2.9.0
|
|
2456
2564
|
[2.8.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.7.0...v2.8.0
|
|
2457
2565
|
[2.7.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.6.0...v2.7.0
|
|
2458
2566
|
[2.6.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.5.0...v2.6.0
|
package/README.md
CHANGED
|
@@ -156,6 +156,7 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
156
156
|
| [Retrieval](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/retrieval.md) | Hybrid search, reranking, MMR, parent documents, and Redis, Pinecone, Weaviate, and Chroma stores |
|
|
157
157
|
| [Embeddings and retrieval](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/embeddings.md) | Embeddings as a routed operation family, and RAG helpers |
|
|
158
158
|
| [Media understanding](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/media.md) | Reading documents and images page by page, describing images, and answering questions about them, through any vision model or OCR |
|
|
159
|
+
| [Video generation](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/video.md) | Videos from a prompt or a first frame, on Google Veo and Runway, as durable jobs streamed into an asset store and priced per second |
|
|
159
160
|
| [Images (experimental)](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/images.md) | Generation and masked edits over three backends, input safety, moderation, asset stores |
|
|
160
161
|
| [Voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/voice.md) | Transcription, speech, voice turns, and voice sessions |
|
|
161
162
|
| [Realtime voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/realtime.md) | Browser and server realtime sessions with barge-in, tools, and exports |
|
|
@@ -217,11 +218,13 @@ interface, or enum, has no doc comment, and `npm run docs:guides` fails when a g
|
|
|
217
218
|
every export of the entry points it covers, or its generated reference is stale. Both hold at 100%;
|
|
218
219
|
`--list` names what is missing, and `npm run docs:update` regenerates the references.
|
|
219
220
|
|
|
220
|
-
Real provider conformance,
|
|
221
|
+
Real provider conformance, the vector store contract against real servers, and the experimental
|
|
222
|
+
families against their live services are opt-in:
|
|
221
223
|
|
|
222
224
|
```bash
|
|
223
225
|
npm run test:conformance:real
|
|
224
226
|
npm run test:vectors:live
|
|
227
|
+
npm run test:families:live
|
|
225
228
|
```
|
|
226
229
|
|
|
227
230
|
## Roadmap
|
|
@@ -285,12 +288,20 @@ Shipped so far:
|
|
|
285
288
|
Chroma stores out of experimental after their checklist ran against the real servers; a shared
|
|
286
289
|
embedding cache; images, audio, and documents as embedding input; a half-price embedding batch tier
|
|
287
290
|
that ingestion survives a crash through; streaming asset stores; registry data you supply and
|
|
288
|
-
refresh; and reading, describing, and searching media, with answers given the media themselves
|
|
291
|
+
refresh; and reading, describing, and searching media, with answers given the media themselves;
|
|
292
|
+
- 2.9 video, and everything shipped, proven: videos from a prompt or a first frame on Google Veo and
|
|
293
|
+
Runway, as durable jobs another worker collects after a crash, streamed into an asset store and
|
|
294
|
+
priced per second; online evaluation that reads every run however many share a millisecond, and
|
|
295
|
+
budget reservations a dead worker leaves settled by what its call did; a deep agent's helper limit
|
|
296
|
+
across processes; a media index that remembers what it holds; and the experimental families run
|
|
297
|
+
against their live services every week.
|
|
289
298
|
|
|
290
299
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
291
300
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
292
|
-
Next,
|
|
293
|
-
|
|
301
|
+
Next, 2.10 makes every conversation that runs on a worker durable, A2A tasks, ACP turns, and
|
|
302
|
+
realtime sessions, and adds Gemini Live as a second realtime provider; Pinecone, images, the Gemini
|
|
303
|
+
embedding batch tier, media understanding, and video graduate once the live workflow passes against
|
|
304
|
+
each. The
|
|
294
305
|
[roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
|
|
295
306
|
applications ask for it.
|
|
296
307
|
|
package/dist/batch/poll.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BatchJobState } from '../types/batch.js';
|
|
2
|
-
/** How a batch is waited on. */
|
|
3
|
-
export interface BatchPollOptions {
|
|
2
|
+
/** How a batch, or any job with a status, is waited on. */
|
|
3
|
+
export interface BatchPollOptions<State = BatchJobState> {
|
|
4
4
|
/** The first wait between polls, in milliseconds. */
|
|
5
5
|
intervalMs: number;
|
|
6
6
|
/** The longest wait, once backoff has grown it. */
|
|
@@ -8,11 +8,15 @@ export interface BatchPollOptions {
|
|
|
8
8
|
/** Stops waiting. The provider-side batch keeps running. */
|
|
9
9
|
signal: AbortSignal;
|
|
10
10
|
/** Called with each state that is not yet final. */
|
|
11
|
-
onState?: (state:
|
|
11
|
+
onState?: (state: State) => void;
|
|
12
|
+
/** Whether a state is final. Defaults to a batch's terminal statuses. */
|
|
13
|
+
settled?: (state: State) => boolean;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Polls a batch until it reaches a status it never leaves. A batch measured in hours does not deserve
|
|
15
17
|
* a fixed short interval: the wait grows by half each time up to the maximum, which keeps the poll
|
|
16
18
|
* count sane without delaying a fast batch, which the first few short polls still catch.
|
|
17
19
|
*/
|
|
18
|
-
export declare function pollUntilSettled
|
|
20
|
+
export declare function pollUntilSettled<State extends {
|
|
21
|
+
status: string;
|
|
22
|
+
} = BatchJobState>(poll: () => Promise<State>, options: BatchPollOptions<State>): Promise<State>;
|
package/dist/batch/poll.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { isTerminalBatchStatus } from '../types/batch.js';
|
|
2
2
|
export async function pollUntilSettled(poll, options) {
|
|
3
|
+
const settled = options.settled ?? ((state) => isTerminalBatchStatus(state.status));
|
|
3
4
|
let interval = options.intervalMs;
|
|
4
5
|
let state = await poll();
|
|
5
|
-
while (!
|
|
6
|
+
while (!settled(state)) {
|
|
6
7
|
if (options.signal.aborted)
|
|
7
8
|
throw options.signal.reason ?? new Error('aborted');
|
|
8
9
|
options.onState?.(state);
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* its checkpoints to the deep agent's checkpointer. A crash leaves the helper's finished steps behind
|
|
7
7
|
* it: collecting the task in a new process carries the helper on from its last checkpoint, and a call
|
|
8
8
|
* the agent repeats after the crash finds the same thread instead of starting the helper again.
|
|
9
|
+
*
|
|
10
|
+
* The helpers an agent creates are recorded on a thread of their own beside its own, before the call
|
|
11
|
+
* that creates one returns, so an agent resumed in another process counts them against its `max` and
|
|
12
|
+
* delegates to them again by name.
|
|
9
13
|
*/
|
|
10
14
|
import { type CreateAgentOptions } from '../agent/create-agent.js';
|
|
11
15
|
import type { PermissionPolicyLike } from '../agent/permissions.js';
|
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
import { agentInput, createAgent, } from '../agent/create-agent.js';
|
|
2
2
|
const MARK = 'deepAgentHelper';
|
|
3
|
+
const CREATED = 'deepAgentCreatedHelpers';
|
|
3
4
|
export function helperTools(setup) {
|
|
4
5
|
const declared = new Map(setup.declared.map((helper) => [helper.name, helper]));
|
|
5
6
|
const created = new Map();
|
|
6
7
|
const graphs = new Map();
|
|
7
8
|
const live = new Map();
|
|
8
|
-
const
|
|
9
|
+
const recorded = new Map();
|
|
10
|
+
const createdThread = (thread) => `${thread || 'thread'}/helpers-created`;
|
|
11
|
+
const loadCreated = (thread) => {
|
|
12
|
+
const checkpointer = setup.checkpointer;
|
|
13
|
+
if (!checkpointer || !setup.create)
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
let loading = recorded.get(thread);
|
|
16
|
+
if (!loading) {
|
|
17
|
+
loading = (async () => {
|
|
18
|
+
const stored = await checkpointer.get(createdThread(thread));
|
|
19
|
+
const records = (stored?.metadata?.[CREATED] ?? []);
|
|
20
|
+
const mine = created.get(thread) ?? new Map();
|
|
21
|
+
for (const record of records) {
|
|
22
|
+
if (!mine.has(record.name))
|
|
23
|
+
mine.set(record.name, { ...record, created: true });
|
|
24
|
+
}
|
|
25
|
+
created.set(thread, mine);
|
|
26
|
+
})();
|
|
27
|
+
loading.catch(() => recorded.delete(thread));
|
|
28
|
+
recorded.set(thread, loading);
|
|
29
|
+
}
|
|
30
|
+
return loading;
|
|
31
|
+
};
|
|
32
|
+
const recordCreated = async (thread, mine) => {
|
|
33
|
+
const checkpointer = setup.checkpointer;
|
|
34
|
+
if (!checkpointer)
|
|
35
|
+
return;
|
|
36
|
+
const threadId = createdThread(thread);
|
|
37
|
+
const records = [...mine.values()].map(({ created: _created, ...record }) => record);
|
|
38
|
+
await checkpointer.put({
|
|
39
|
+
version: 2,
|
|
40
|
+
id: `${threadId}:${records.length}`,
|
|
41
|
+
threadId,
|
|
42
|
+
step: records.length,
|
|
43
|
+
state: {},
|
|
44
|
+
next: [],
|
|
45
|
+
tasks: [],
|
|
46
|
+
status: 'completed',
|
|
47
|
+
interrupts: [],
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
metadata: { [CREATED]: records },
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const spec = async (name, instructions, thread) => {
|
|
9
53
|
const helper = declared.get(name);
|
|
10
54
|
if (helper) {
|
|
11
55
|
return {
|
|
@@ -17,6 +61,7 @@ export function helperTools(setup) {
|
|
|
17
61
|
created: false,
|
|
18
62
|
};
|
|
19
63
|
}
|
|
64
|
+
await loadCreated(thread);
|
|
20
65
|
const mine = created.get(thread) ?? new Map();
|
|
21
66
|
const existing = mine.get(name);
|
|
22
67
|
if (existing)
|
|
@@ -42,6 +87,13 @@ export function helperTools(setup) {
|
|
|
42
87
|
};
|
|
43
88
|
mine.set(name, made);
|
|
44
89
|
created.set(thread, mine);
|
|
90
|
+
try {
|
|
91
|
+
await recordCreated(thread, mine);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
mine.delete(name);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
45
97
|
return made;
|
|
46
98
|
};
|
|
47
99
|
const graph = (helper, durable) => {
|
|
@@ -165,7 +217,7 @@ export function helperTools(setup) {
|
|
|
165
217
|
},
|
|
166
218
|
capabilities: [],
|
|
167
219
|
execute: async (args, context = {}) => {
|
|
168
|
-
const helper = spec(String(args.agent ?? ''), typeof args.instructions === 'string' ? args.instructions : undefined, context.threadId ?? '');
|
|
220
|
+
const helper = await spec(String(args.agent ?? ''), typeof args.instructions === 'string' ? args.instructions : undefined, context.threadId ?? '');
|
|
169
221
|
const task = String(args.task ?? '');
|
|
170
222
|
const call = context.toolCallId ?? Math.random().toString(36).slice(2, 10);
|
|
171
223
|
if (args.background !== true) {
|
|
@@ -211,8 +263,8 @@ export function helperTools(setup) {
|
|
|
211
263
|
const mark = stored.metadata?.[MARK];
|
|
212
264
|
const name = mark?.name ?? task.slice(0, task.lastIndexOf('@'));
|
|
213
265
|
const helper = mark?.created
|
|
214
|
-
? spec(name, mark.instructions, context.threadId ?? '')
|
|
215
|
-
: spec(name, undefined, context.threadId ?? '');
|
|
266
|
+
? await spec(name, mark.instructions, context.threadId ?? '')
|
|
267
|
+
: await spec(name, undefined, context.threadId ?? '');
|
|
216
268
|
await start(helper, undefined, threadId, context);
|
|
217
269
|
}
|
|
218
270
|
const entry = live.get(threadId);
|
|
@@ -62,6 +62,11 @@ export interface OnlineSchedulePass {
|
|
|
62
62
|
throttled: boolean;
|
|
63
63
|
/** True when the pass read back to the oldest matching run, so the next one starts at the newest. */
|
|
64
64
|
exhausted: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* How the pass paged: by cursor, or by offset on a trace store that does not honour
|
|
67
|
+
* `RunQuery.after`, where runs that start in the same millisecond may be read twice but never skipped.
|
|
68
|
+
*/
|
|
69
|
+
paging: 'cursor' | 'offset';
|
|
65
70
|
}
|
|
66
71
|
/** What a worker has done since it was created. */
|
|
67
72
|
export interface OnlineEvaluationWorkerReport {
|
package/dist/evaluate/online.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BudgetExceededError } from '../core/lifecycle.js';
|
|
2
2
|
import { operationStats } from '../operations/store.js';
|
|
3
3
|
import { OperationRunner } from '../operations/runner.js';
|
|
4
|
+
import { isAfterCursor, runCursor } from '../tracing/query.js';
|
|
4
5
|
class HandBack extends Error {
|
|
5
6
|
}
|
|
6
7
|
export function onlineEvaluationWorker(options) {
|
|
@@ -24,6 +25,25 @@ export function onlineEvaluationWorker(options) {
|
|
|
24
25
|
requestId,
|
|
25
26
|
...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
|
|
26
27
|
});
|
|
28
|
+
const outcomeOf = (evaluating, run, operationId) => {
|
|
29
|
+
const scores = run ? scoresWritten(run, options.source, operationId) : [];
|
|
30
|
+
if (scores.length > 0) {
|
|
31
|
+
return { kind: 'finished', cost: (run && options.costOf?.(scores, run)) ?? options.estimate ?? 0 };
|
|
32
|
+
}
|
|
33
|
+
return evaluating ? { kind: 'unknown' } : { kind: 'never-started' };
|
|
34
|
+
};
|
|
35
|
+
const resolveReservation = async (reservation) => {
|
|
36
|
+
const record = await options.operations.read(reservation.operationId);
|
|
37
|
+
if (record?.kind !== kind)
|
|
38
|
+
return undefined;
|
|
39
|
+
const checkpoint = (record.heartbeatDetails ?? {});
|
|
40
|
+
const mine = checkpoint.reservation === reservation.id;
|
|
41
|
+
const live = record.status === 'running' && record.lease !== undefined && Date.parse(record.lease.expiresAt) > now().getTime();
|
|
42
|
+
if (live && mine)
|
|
43
|
+
return { kind: 'running' };
|
|
44
|
+
const run = await options.traces.get(String(record.metadata?.runId ?? ''));
|
|
45
|
+
return mine ? outcomeOf(checkpoint.evaluating === true, run, reservation.operationId) : { kind: 'never-started' };
|
|
46
|
+
};
|
|
27
47
|
const inSample = (run) => rate >= 1 || hash(`${options.source}:${run.id}`) < rate;
|
|
28
48
|
const operationId = (runId) => `${kind}:${runId}`;
|
|
29
49
|
const busy = async () => {
|
|
@@ -38,6 +58,10 @@ export function onlineEvaluationWorker(options) {
|
|
|
38
58
|
}
|
|
39
59
|
const runId = String(record.metadata?.runId ?? '');
|
|
40
60
|
const run = await options.traces.get(runId);
|
|
61
|
+
const previous = context.previousHeartbeat;
|
|
62
|
+
if (previous?.reservation && options.budget?.settle) {
|
|
63
|
+
await options.budget.settle(previous.reservation, outcomeOf(previous.evaluating === true, run, context.operationId));
|
|
64
|
+
}
|
|
41
65
|
if (!run) {
|
|
42
66
|
tally.missing += 1;
|
|
43
67
|
return { status: 'missing', runId };
|
|
@@ -47,7 +71,7 @@ export function onlineEvaluationWorker(options) {
|
|
|
47
71
|
entry.value?.evaluation === context.operationId)
|
|
48
72
|
.map((entry) => entry.key));
|
|
49
73
|
let reservation;
|
|
50
|
-
if (options.budget
|
|
74
|
+
if (options.budget) {
|
|
51
75
|
try {
|
|
52
76
|
reservation = await options.budget.reserve(descriptor(context.operationId), options.estimate ?? 0);
|
|
53
77
|
}
|
|
@@ -60,9 +84,16 @@ export function onlineEvaluationWorker(options) {
|
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
try {
|
|
87
|
+
if (reservation?.id) {
|
|
88
|
+
await context.heartbeat({ reservation: reservation.id, evaluating: true });
|
|
89
|
+
}
|
|
63
90
|
const scores = await scoreRun(run, options.evaluators);
|
|
64
91
|
const missing = scores.filter((score) => !written.has(score.key));
|
|
65
92
|
if (missing.length === 0 && written.size > 0) {
|
|
93
|
+
if (reservation) {
|
|
94
|
+
await options.budget?.reconcile(reservation, options.costOf?.(scores, run) ?? options.estimate ?? 0);
|
|
95
|
+
reservation = undefined;
|
|
96
|
+
}
|
|
66
97
|
tally.alreadyScored += 1;
|
|
67
98
|
return { status: 'already-scored', runId };
|
|
68
99
|
}
|
|
@@ -98,17 +129,35 @@ export function onlineEvaluationWorker(options) {
|
|
|
98
129
|
return runner.workerId;
|
|
99
130
|
},
|
|
100
131
|
async schedule() {
|
|
101
|
-
const pass = {
|
|
132
|
+
const pass = {
|
|
133
|
+
scanned: 0,
|
|
134
|
+
sampled: 0,
|
|
135
|
+
queued: 0,
|
|
136
|
+
throttled: false,
|
|
137
|
+
exhausted: false,
|
|
138
|
+
paging: byOffset ? 'offset' : 'cursor',
|
|
139
|
+
};
|
|
102
140
|
if (budgetSpent)
|
|
103
141
|
return pass;
|
|
104
142
|
const limit = options.scanLimit ?? 1_000;
|
|
105
|
-
const
|
|
143
|
+
const read = () => options.traces.query({
|
|
106
144
|
kind: 'model',
|
|
107
145
|
status: 'ok',
|
|
108
146
|
...options.query,
|
|
109
|
-
...(cursor ? {
|
|
147
|
+
...(byOffset ? { offset } : cursor ? { after: cursor } : {}),
|
|
110
148
|
limit,
|
|
111
149
|
});
|
|
150
|
+
let runs = await read();
|
|
151
|
+
if (!byOffset && cursor) {
|
|
152
|
+
const from = cursor;
|
|
153
|
+
if (runs.some((run) => !isAfterCursor(run, from))) {
|
|
154
|
+
byOffset = true;
|
|
155
|
+
pass.paging = 'offset';
|
|
156
|
+
cursor = undefined;
|
|
157
|
+
offset = 0;
|
|
158
|
+
runs = await read();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
112
161
|
pass.scanned = runs.length;
|
|
113
162
|
const maxQueued = options.maxQueued ?? 1_000;
|
|
114
163
|
let open = await busy();
|
|
@@ -146,9 +195,12 @@ export function onlineEvaluationWorker(options) {
|
|
|
146
195
|
if (runs.length < limit) {
|
|
147
196
|
pass.exhausted = true;
|
|
148
197
|
cursor = undefined;
|
|
198
|
+
offset = 0;
|
|
149
199
|
}
|
|
150
200
|
else if (pass.queued === 0) {
|
|
151
|
-
|
|
201
|
+
const last = runs.at(-1);
|
|
202
|
+
cursor = last ? runCursor(last) : undefined;
|
|
203
|
+
offset += runs.length;
|
|
152
204
|
}
|
|
153
205
|
}
|
|
154
206
|
return pass;
|
|
@@ -156,6 +208,12 @@ export function onlineEvaluationWorker(options) {
|
|
|
156
208
|
async work() {
|
|
157
209
|
if (budgetSpent)
|
|
158
210
|
return 0;
|
|
211
|
+
if (options.budget?.recover) {
|
|
212
|
+
await options.budget.recover(resolveReservation, {
|
|
213
|
+
operation: `online-evaluation.${options.source}`,
|
|
214
|
+
limit: concurrency * 4,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
159
217
|
const handles = [
|
|
160
218
|
...(await runner.recover(evaluate, concurrency)),
|
|
161
219
|
...(await runner.claimQueued(evaluate, concurrency, { kindPrefix: kind })),
|
|
@@ -218,8 +276,19 @@ export function onlineEvaluationWorker(options) {
|
|
|
218
276
|
let running;
|
|
219
277
|
let stopping;
|
|
220
278
|
let cursor;
|
|
279
|
+
let offset = 0;
|
|
280
|
+
let byOffset = false;
|
|
221
281
|
return worker;
|
|
222
282
|
}
|
|
283
|
+
function scoresWritten(run, source, operationId) {
|
|
284
|
+
return (run.feedback ?? [])
|
|
285
|
+
.filter((entry) => entry.source === source && entry.value?.evaluation === operationId)
|
|
286
|
+
.map((entry) => ({
|
|
287
|
+
key: entry.key,
|
|
288
|
+
score: entry.score ?? 0,
|
|
289
|
+
...(entry.comment ? { comment: entry.comment } : {}),
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
223
292
|
async function scoreRun(run, evaluators) {
|
|
224
293
|
const context = {
|
|
225
294
|
example: { id: run.id, inputs: run.inputs, expected: undefined },
|
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
import type { BudgetLedger, BudgetPeriod, OperationDescriptor } from '../types/lifecycle.js';
|
|
1
|
+
import type { BudgetLedger, BudgetPeriod, BudgetReservation, BudgetReservationOutcome, BudgetReservationRecord, OperationDescriptor } from '../types/lifecycle.js';
|
|
2
|
+
import { type BudgetReservationChange } from './reservations.js';
|
|
3
|
+
export type { BudgetReservationChange } from './reservations.js';
|
|
2
4
|
/**
|
|
3
5
|
* Where a ledger keeps its totals: a sum per key that resets at a given time. Two operations, so any
|
|
4
6
|
* shared store can implement it. `MemoryTenantUsage` and `RedisTenantUsage` from
|
|
5
7
|
* `nexus-ai-pro/server/tenancy` already do, so one Redis counts both tenant runs and model spend.
|
|
8
|
+
*
|
|
9
|
+
* A store that also records reservations implements the five optional methods together. A ledger
|
|
10
|
+
* over it records each reservation under a lease, so one a dead process held is found and settled,
|
|
11
|
+
* and moves each reservation and its total in one step, so a crash never applies one without the
|
|
12
|
+
* other. Every bundled store does.
|
|
6
13
|
*/
|
|
7
14
|
export interface BudgetUsageStore {
|
|
8
15
|
/** Adds to a total that resets at `resetAt`, epoch milliseconds, and resolves to the new total. */
|
|
9
16
|
add(key: string, amount: number, resetAt: number): number | Promise<number>;
|
|
10
17
|
/** A total, or 0. */
|
|
11
18
|
total(key: string): number | Promise<number>;
|
|
19
|
+
/** Records a reservation and adds what it charges to its total, together: resolves to the new total. */
|
|
20
|
+
hold?(record: BudgetReservationRecord): number | Promise<number>;
|
|
21
|
+
/**
|
|
22
|
+
* Moves a reservation and its total, by the difference in what it charges, together, if it stands
|
|
23
|
+
* where `change.from` allows. Resolves whether it moved.
|
|
24
|
+
*/
|
|
25
|
+
settle?(id: string, change: BudgetReservationChange): boolean | Promise<boolean>;
|
|
26
|
+
/** Extends a reserved reservation's lease. Resolves whether it was still reserved. */
|
|
27
|
+
renew?(id: string, leasedUntil: number): boolean | Promise<boolean>;
|
|
28
|
+
/** Reserved reservations whose lease lapsed by `now`, soonest lapsed first, optionally for one operation. */
|
|
29
|
+
lapsed?(now: number, limit: number, operation?: string): BudgetReservationRecord[] | Promise<BudgetReservationRecord[]>;
|
|
30
|
+
/** One reservation, while the store keeps it. */
|
|
31
|
+
reservation?(id: string): BudgetReservationRecord | undefined | Promise<BudgetReservationRecord | undefined>;
|
|
12
32
|
}
|
|
13
33
|
/** Options for `budgetLedger()`. */
|
|
14
34
|
export interface BudgetLedgerOptions {
|
|
@@ -26,13 +46,34 @@ export interface BudgetLedgerOptions {
|
|
|
26
46
|
key?: (operation: OperationDescriptor) => string | undefined;
|
|
27
47
|
/** Where totals are kept. Defaults to process memory, which is enough for one replica. */
|
|
28
48
|
store?: BudgetUsageStore;
|
|
49
|
+
/**
|
|
50
|
+
* How long a recorded reservation is held without `renew()` before `recover()` may settle it.
|
|
51
|
+
* Defaults to 10 minutes: longer than the operations it covers take.
|
|
52
|
+
*/
|
|
53
|
+
leaseMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* What a reservation whose operation's outcome is unknown costs until its operation says: `charge`
|
|
56
|
+
* keeps it charged, as a provider may have billed the call; `release` gives it back. Defaults to
|
|
57
|
+
* `charge`.
|
|
58
|
+
*/
|
|
59
|
+
uncertain?: 'charge' | 'release';
|
|
29
60
|
/** Replaces the clock, for tests. */
|
|
30
61
|
now?: () => Date;
|
|
31
62
|
}
|
|
32
|
-
/** A ledger that can also say what a budget has spent this period. */
|
|
63
|
+
/** A ledger that can also say what a budget has spent this period, and settle what a dead process held. */
|
|
33
64
|
export interface BudgetLedgerWithReport extends BudgetLedger {
|
|
65
|
+
/** Holds `estimate` for the operation, or rejects with `BudgetExceededError` when it does not fit. */
|
|
66
|
+
reserve(operation: OperationDescriptor, estimate: number): Promise<BudgetReservation | undefined>;
|
|
34
67
|
/** What a budget has spent so far this period, in US dollars, reservations included. */
|
|
35
68
|
spent(key: string): Promise<number>;
|
|
69
|
+
/** A recorded reservation, while the store keeps it. */
|
|
70
|
+
reservation(id: string): Promise<BudgetReservationRecord | undefined>;
|
|
71
|
+
/** Extends a recorded reservation's lease. Does nothing for one the store does not record. */
|
|
72
|
+
renew(reservation: BudgetReservation): Promise<void>;
|
|
73
|
+
/** Settles a recorded reservation by its id. Resolves `false` on a store that does not record them. */
|
|
74
|
+
settle(id: string, outcome: BudgetReservationOutcome): Promise<boolean>;
|
|
75
|
+
/** Settles reservations whose lease lapsed. Settles nothing on a store that does not record them. */
|
|
76
|
+
recover: NonNullable<BudgetLedger['recover']>;
|
|
36
77
|
}
|
|
37
78
|
/**
|
|
38
79
|
* A spend budget shared by every family of a client.
|
|
@@ -41,15 +82,31 @@ export interface BudgetLedgerWithReport extends BudgetLedger {
|
|
|
41
82
|
* the limit, the addition is undone and `BudgetExceededError` stops the call. Afterwards the estimate
|
|
42
83
|
* is replaced by what the call actually cost, or given back when it failed. Adding first and checking
|
|
43
84
|
* the new total is what keeps concurrent calls from overshooting together, on one process or many.
|
|
85
|
+
*
|
|
86
|
+
* Over a store that records reservations, each is held under a lease. One whose process died is
|
|
87
|
+
* found by `recover()` when its lease lapses and settled by what became of its operation: charged at
|
|
88
|
+
* its cost if it finished, given back if it never started, and charged or given back by `uncertain`
|
|
89
|
+
* when no one knows.
|
|
44
90
|
*/
|
|
45
91
|
export declare function budgetLedger(options: BudgetLedgerOptions): BudgetLedgerWithReport;
|
|
46
|
-
/** Budget totals in process memory. The default store, and enough for one replica. */
|
|
92
|
+
/** Budget totals, and the reservations against them, in process memory. The default store, and enough for one replica. */
|
|
47
93
|
export declare class MemoryBudgetUsage implements BudgetUsageStore {
|
|
48
94
|
private readonly now;
|
|
49
95
|
private readonly totals;
|
|
96
|
+
private readonly book;
|
|
50
97
|
constructor(now?: () => number);
|
|
51
98
|
/** Adds to a total that resets at `resetAt`. */
|
|
52
99
|
add(key: string, amount: number, resetAt: number): number;
|
|
53
100
|
/** A total, or 0 once it has reset. */
|
|
54
101
|
total(key: string): number;
|
|
102
|
+
/** Records a reservation and adds it to its total. */
|
|
103
|
+
hold(record: BudgetReservationRecord): number;
|
|
104
|
+
/** Moves a reservation and its total together. */
|
|
105
|
+
settle(id: string, change: BudgetReservationChange): boolean;
|
|
106
|
+
/** Extends a reserved reservation's lease. */
|
|
107
|
+
renew(id: string, leasedUntil: number): boolean;
|
|
108
|
+
/** Reserved reservations whose lease lapsed. */
|
|
109
|
+
lapsed(now: number, limit: number, operation?: string): BudgetReservationRecord[];
|
|
110
|
+
/** One reservation. */
|
|
111
|
+
reservation(id: string): BudgetReservationRecord | undefined;
|
|
55
112
|
}
|