akanjs 3.1.1-rc.2 → 3.1.1-rc.4
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/client/router.ts +29 -1
- package/document/database.ts +12 -4
- package/document/documentQuery.ts +6 -1
- package/document/into.ts +8 -2
- package/package.json +1 -1
- package/server/di/diLifecycle.ts +11 -1
- package/server/resolver/database.resolver.ts +19 -6
- package/server/resolver/service.resolver.ts +14 -6
- package/service/agent.service.ts +4 -1
- package/service/predefinedAdaptor/openaiLlm.ts +20 -4
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +15 -10
- package/service/predefinedAdaptor/sql/types.ts +2 -1
- package/service/serviceModule.ts +2 -2
- package/service/types.ts +11 -2
- package/store/agent/AgentVisual.ts +2 -14
- package/store/agent/ScreenTarget.ts +25 -0
- package/store/agent/StoreSurfaceSource.ts +27 -6
- package/types/client/router.d.ts +1 -0
- package/types/document/database.d.ts +4 -4
- package/types/document/documentQuery.d.ts +5 -1
- package/types/document/into.d.ts +2 -2
- package/types/service/agent.service.d.ts +2 -1
- package/types/service/predefinedAdaptor/openaiLlm.d.ts +7 -2
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +2 -2
- package/types/service/predefinedAdaptor/sql/types.d.ts +2 -2
- package/types/service/types.d.ts +3 -3
- package/types/store/agent/ScreenTarget.d.ts +3 -0
- package/vendor/use-agentic/AgentSession.ts +18 -1
package/client/router.ts
CHANGED
|
@@ -180,7 +180,7 @@ export const getPathInfo = (requestUrl: string, lang: string, prefix: string) =>
|
|
|
180
180
|
return { path, pathname, hash, search, href };
|
|
181
181
|
};
|
|
182
182
|
class Router {
|
|
183
|
-
isInitialized = false;
|
|
183
|
+
isInitialized: boolean = false;
|
|
184
184
|
#prefix = "";
|
|
185
185
|
#lang = parseAkanI18nEnv().defaultLocale;
|
|
186
186
|
#routePaths = new Set<string>();
|
|
@@ -193,6 +193,8 @@ class Router {
|
|
|
193
193
|
#backsAwaitingPopstate = 0;
|
|
194
194
|
#redirects = 0;
|
|
195
195
|
#navigations = 0;
|
|
196
|
+
|
|
197
|
+
#beforeInit: (() => void)[] = [];
|
|
196
198
|
#instance: RouterInstance = {
|
|
197
199
|
push: (href: string) => {
|
|
198
200
|
const { href: fullHref } = this.#getPathInfo(href);
|
|
@@ -227,6 +229,7 @@ class Router {
|
|
|
227
229
|
else if (options.side === "client") this.#initSsrClientRouter(options);
|
|
228
230
|
this.isInitialized = true;
|
|
229
231
|
Logger.verbose("Router initialized");
|
|
232
|
+
for (const call of this.#beforeInit.splice(0)) call();
|
|
230
233
|
}
|
|
231
234
|
#initSsrClientRouter(options: SsrClientRouterOption) {
|
|
232
235
|
this.#historyWrittenByNavigation = true;
|
|
@@ -282,6 +285,27 @@ class Router {
|
|
|
282
285
|
#checkInitialized() {
|
|
283
286
|
if (!this.isInitialized) throw new Error("Router is not initialized");
|
|
284
287
|
}
|
|
288
|
+
#deferredUntilInit(call: () => void) {
|
|
289
|
+
if (this.isInitialized || getEnv().side === "server") return false;
|
|
290
|
+
this.#track(
|
|
291
|
+
new Promise<void>((resolve, reject) => {
|
|
292
|
+
this.#beforeInit.push(() => {
|
|
293
|
+
const pending = this.#navigation;
|
|
294
|
+
try {
|
|
295
|
+
call();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
Logger.error(`A navigation made before the router was initialized failed: ${String(error)}`);
|
|
298
|
+
reject(error);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (this.#navigation === pending) resolve();
|
|
303
|
+
else this.#navigation.then(resolve, reject);
|
|
304
|
+
});
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
285
309
|
|
|
286
310
|
#ensureHistoryState() {
|
|
287
311
|
if (getEnv().side === "server") return;
|
|
@@ -389,7 +413,9 @@ class Router {
|
|
|
389
413
|
const target = `${path}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
|
|
390
414
|
return dedupedStack.map((candidate, index) => (index === dedupedStack.length - 1 ? target : candidate));
|
|
391
415
|
}
|
|
416
|
+
/** Before init the link waits for it, and the answer is `true`: the manifest that could refuse it is not loaded yet. */
|
|
392
417
|
enterDeepLink(href: string, options: DeepLinkOptions = {}) {
|
|
418
|
+
if (this.#deferredUntilInit(() => this.enterDeepLink(href, options))) return true;
|
|
393
419
|
this.#checkInitialized();
|
|
394
420
|
if (!options.resetStack) {
|
|
395
421
|
this.push(normalizeDeepLinkHref(href), options);
|
|
@@ -405,6 +431,7 @@ class Router {
|
|
|
405
431
|
return true;
|
|
406
432
|
}
|
|
407
433
|
push(href: string, routeOptions?: RouteOptions) {
|
|
434
|
+
if (this.#deferredUntilInit(() => this.push(href, routeOptions))) return undefined as never;
|
|
408
435
|
this.#checkInitialized();
|
|
409
436
|
if (this.#refusesOffsite("push", href, routeOptions)) return undefined as never;
|
|
410
437
|
this.#navigations += 1;
|
|
@@ -414,6 +441,7 @@ class Router {
|
|
|
414
441
|
return undefined as never;
|
|
415
442
|
}
|
|
416
443
|
replace(href: string, routeOptions?: RouteOptions) {
|
|
444
|
+
if (this.#deferredUntilInit(() => this.replace(href, routeOptions))) return undefined as never;
|
|
417
445
|
this.#checkInitialized();
|
|
418
446
|
if (this.#refusesOffsite("replace", href, routeOptions)) return undefined as never;
|
|
419
447
|
this.#navigations += 1;
|
package/document/database.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Logger } from "akanjs/common";
|
|
|
3
3
|
import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
4
4
|
import type { CacheAdaptor, CacheSetOptions, CacheValue } from "akanjs/service";
|
|
5
5
|
import type { DataLoader } from "./dataLoader";
|
|
6
|
-
import type { DocumentUpdateInput } from "./documentQuery";
|
|
6
|
+
import type { DocumentTouchOptions, DocumentUpdateInput } from "./documentQuery";
|
|
7
7
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta";
|
|
8
8
|
import type { Mdl, SaveEventListener, SaveEventType, UpdateResult } from "./into";
|
|
9
9
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types";
|
|
@@ -61,7 +61,7 @@ export class CacheDatabase<_T = unknown> {
|
|
|
61
61
|
}
|
|
62
62
|
/** What `update<Filter>` returns: the patch cannot trail a filter's optional args, so it lands on `.set()`. */
|
|
63
63
|
export interface UpdateChain<Doc = any> {
|
|
64
|
-
set(update: DocumentUpdateInput<Doc
|
|
64
|
+
set(update: DocumentUpdateInput<Doc>, options?: DocumentTouchOptions): Promise<UpdateResult>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
type QueryMethodOfKey<
|
|
@@ -157,8 +157,16 @@ export type DatabaseInstanceWithQuerySort<
|
|
|
157
157
|
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
158
158
|
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
159
159
|
__purgeMany: (query: _QueryOfDoc) => Promise<number>;
|
|
160
|
-
__updateMany: (
|
|
161
|
-
|
|
160
|
+
__updateMany: (
|
|
161
|
+
query: _QueryOfDoc,
|
|
162
|
+
update: DocumentUpdateInput<Doc>,
|
|
163
|
+
options?: DocumentTouchOptions,
|
|
164
|
+
) => Promise<UpdateResult>;
|
|
165
|
+
__updateOne: (
|
|
166
|
+
query: _QueryOfDoc,
|
|
167
|
+
update: DocumentUpdateInput<Doc>,
|
|
168
|
+
options?: DocumentTouchOptions,
|
|
169
|
+
) => Promise<UpdateResult>;
|
|
162
170
|
__list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
163
171
|
__listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
164
172
|
__find(query: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|
|
@@ -106,7 +106,12 @@ export interface DocumentScopeOptions {
|
|
|
106
106
|
withRemoved?: boolean;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export interface
|
|
109
|
+
export interface DocumentTouchOptions {
|
|
110
|
+
/** `false` leaves `updatedAt` as stored, for bookkeeping that is no edit: a lease heartbeat, a mirrored snapshot. */
|
|
111
|
+
touch?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface DocumentUpdateOptions extends DocumentScopeOptions, DocumentTouchOptions {
|
|
110
115
|
upsert?: boolean;
|
|
111
116
|
}
|
|
112
117
|
|
package/document/into.ts
CHANGED
|
@@ -4,7 +4,13 @@ import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
|
4
4
|
import type { FilterCls, FilterQueryOf, FilterSortOf, SchemaOf } from ".";
|
|
5
5
|
import type { CacheDatabase, QueryMethodPart } from "./database";
|
|
6
6
|
import type { DataLoader } from "./dataLoader";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
DocumentQuery,
|
|
9
|
+
DocumentScopeOptions,
|
|
10
|
+
DocumentTouchOptions,
|
|
11
|
+
DocumentUpdateInput,
|
|
12
|
+
DocumentUpdateOptions,
|
|
13
|
+
} from "./documentQuery";
|
|
8
14
|
import { type LoaderBuilder, type ModelCls, makeLoaderBuilder } from "./loaderInfo";
|
|
9
15
|
import type { DocumentProjection } from "./types";
|
|
10
16
|
|
|
@@ -79,7 +85,7 @@ export type Mdl<
|
|
|
79
85
|
updateMany(
|
|
80
86
|
query: _RawQuery,
|
|
81
87
|
update: DocumentUpdateInput<_RawDoc>,
|
|
82
|
-
options?: DocumentScopeOptions,
|
|
88
|
+
options?: DocumentScopeOptions & DocumentTouchOptions,
|
|
83
89
|
): Promise<UpdateResult>;
|
|
84
90
|
removeOne(query: _RawQuery): Promise<UpdateResult>;
|
|
85
91
|
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
package/package.json
CHANGED
package/server/di/diLifecycle.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BackendEnv, DatabaseModes, getEnv } from "akanjs/base";
|
|
1
|
+
import { type BackendEnv, DatabaseModes, getEnv, INTERNAL_META } from "akanjs/base";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
3
|
import { ConstantRegistry } from "akanjs/constant";
|
|
4
4
|
import {
|
|
@@ -195,6 +195,7 @@ export class DiLifecycle {
|
|
|
195
195
|
this.logger.info("agent relay is provided by a lib module — the framework's is skipped");
|
|
196
196
|
if (!this.#scalar.has("agentTurn"))
|
|
197
197
|
this.#scalar.set("agentTurn", { constant: agentTurnConstant, database: agentTurnDocument });
|
|
198
|
+
this.#assertUniqueInternalJobs();
|
|
198
199
|
const adaptorClaims = new Map<string, AdaptorCls>();
|
|
199
200
|
const adaptorRegistrations: Registration[] = [];
|
|
200
201
|
const claimAdaptor = (adaptorCls: AdaptorCls, owner: string) => {
|
|
@@ -460,6 +461,15 @@ export class DiLifecycle {
|
|
|
460
461
|
this.logger.verbose(`${doneLabel} in ${Date.now() - now}ms`);
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
#assertUniqueInternalJobs() {
|
|
465
|
+
const jobs = this.#allModules().flatMap(({ signal: { internal } }) =>
|
|
466
|
+
Object.entries(internal[INTERNAL_META])
|
|
467
|
+
.filter(([, { type }]) => type !== "resolveField")
|
|
468
|
+
.map(([key, { type }]) => ({ key, owner: `the ${type} of "${internal.refName}"` })),
|
|
469
|
+
);
|
|
470
|
+
assertUniqueRegistrations("internal", jobs);
|
|
471
|
+
}
|
|
472
|
+
|
|
463
473
|
registerSchedule(serverMode: "federation" | "batch" | "all") {
|
|
464
474
|
const internals = this.#allModules().map((mod) => mod.signal.internal);
|
|
465
475
|
const failures: { label: string; reason: unknown }[] = [];
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DataLoader,
|
|
12
12
|
DocumentSchema,
|
|
13
13
|
type DocumentScopeOptions,
|
|
14
|
+
type DocumentTouchOptions,
|
|
14
15
|
type DocumentUpdateInput,
|
|
15
16
|
type DocumentUpdateOptions,
|
|
16
17
|
documentQueryHelper,
|
|
@@ -45,6 +46,9 @@ const timedQuery = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
|
45
46
|
trace.countDbQuery(performance.now() - start);
|
|
46
47
|
}
|
|
47
48
|
};
|
|
49
|
+
const touchOf = (options?: DocumentTouchOptions) => (options?.touch === false ? { touch: false } : {});
|
|
50
|
+
const touchArgs = (options?: DocumentTouchOptions): [] | [DocumentTouchOptions] =>
|
|
51
|
+
options?.touch === false ? [{ touch: false }] : [];
|
|
48
52
|
|
|
49
53
|
export class DatabaseResolver {
|
|
50
54
|
|
|
@@ -205,6 +209,12 @@ export class DatabaseResolver {
|
|
|
205
209
|
const scopeOf = (options?: DocumentScopeOptions) => (options?.withRemoved ? { withRemoved: true } : {});
|
|
206
210
|
const scopeArgs = (options?: DocumentScopeOptions): [] | [DocumentScopeOptions] =>
|
|
207
211
|
options?.withRemoved ? [{ withRemoved: true }] : [];
|
|
212
|
+
const updateManyArgs = (
|
|
213
|
+
options?: DocumentScopeOptions & DocumentTouchOptions,
|
|
214
|
+
): [] | [DocumentScopeOptions & DocumentTouchOptions] => {
|
|
215
|
+
const narrowed = { ...scopeOf(options), ...touchOf(options) };
|
|
216
|
+
return Object.keys(narrowed).length ? [narrowed] : [];
|
|
217
|
+
};
|
|
208
218
|
const createFindManyChain = (
|
|
209
219
|
query: QueryOf<any>,
|
|
210
220
|
options: { sort?: any; skip?: number; limit?: number; select?: any; withRemoved?: boolean } = {},
|
|
@@ -287,8 +297,11 @@ export class DatabaseResolver {
|
|
|
287
297
|
timedQuery(() => store.count(query, ...scopeArgs(options))),
|
|
288
298
|
updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: DocumentUpdateOptions) =>
|
|
289
299
|
timedQuery(() => store.updateOneByQuery(query, update, options)),
|
|
290
|
-
updateMany: (
|
|
291
|
-
|
|
300
|
+
updateMany: (
|
|
301
|
+
query: QueryOf<any>,
|
|
302
|
+
update: DocumentUpdateInput,
|
|
303
|
+
options?: DocumentScopeOptions & DocumentTouchOptions,
|
|
304
|
+
) => timedQuery(() => store.updateManyByQuery(query, update, ...updateManyArgs(options))),
|
|
292
305
|
removeOne: (query: QueryOf<any>) => timedQuery(() => store.removeOneByQuery(query)),
|
|
293
306
|
removeMany: (query: QueryOf<any>) => timedQuery(() => store.removeManyByQuery(query)),
|
|
294
307
|
purgeMany: (query: QueryOf<any>) => timedQuery(() => store.purgeManyByQuery(query)),
|
|
@@ -401,11 +414,11 @@ export class DatabaseResolver {
|
|
|
401
414
|
async __purgeMany(query: QueryOf<any>) {
|
|
402
415
|
return await timedQuery(() => this.__store.purgeManyByQuery(query));
|
|
403
416
|
}
|
|
404
|
-
async __updateMany(query: QueryOf<any>, update: DocumentUpdateInput) {
|
|
405
|
-
return await timedQuery(() => this.__store.updateManyByQuery(query, update));
|
|
417
|
+
async __updateMany(query: QueryOf<any>, update: DocumentUpdateInput, options?: DocumentTouchOptions) {
|
|
418
|
+
return await timedQuery(() => this.__store.updateManyByQuery(query, update, ...touchArgs(options)));
|
|
406
419
|
}
|
|
407
|
-
async __updateOne(query: QueryOf<any>, update: DocumentUpdateInput) {
|
|
408
|
-
return await timedQuery(() => this.__store.updateOneByQuery(query, update));
|
|
420
|
+
async __updateOne(query: QueryOf<any>, update: DocumentUpdateInput, options?: DocumentTouchOptions) {
|
|
421
|
+
return await timedQuery(() => this.__store.updateOneByQuery(query, update, ...touchArgs(options)));
|
|
409
422
|
}
|
|
410
423
|
async [`remove${className}`](id: string) {
|
|
411
424
|
return this.__remove(id);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { capitalize } from "akanjs/common";
|
|
2
2
|
import type { QueryOf } from "akanjs/constant";
|
|
3
|
-
import type { DatabaseModel, DataInputOf, Doc, DocumentUpdateInput } from "akanjs/document";
|
|
3
|
+
import type { DatabaseModel, DataInputOf, Doc, DocumentTouchOptions, DocumentUpdateInput } from "akanjs/document";
|
|
4
4
|
import { type DatabaseService, type ServiceCls, ServiceModel } from "akanjs/service";
|
|
5
5
|
import type { CascadeRunner } from "./CascadeRunner";
|
|
6
6
|
import { DatabaseResolver } from "./database.resolver";
|
|
@@ -38,11 +38,19 @@ export class ServiceResolver {
|
|
|
38
38
|
async __purgeMany(this: DatabaseService, query: QueryOf<any>) {
|
|
39
39
|
return await this.__databaseModel.__purgeMany(query);
|
|
40
40
|
},
|
|
41
|
-
async __updateMany(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
async __updateMany(
|
|
42
|
+
this: DatabaseService,
|
|
43
|
+
query: QueryOf<any>,
|
|
44
|
+
...set: [DocumentUpdateInput, DocumentTouchOptions?]
|
|
45
|
+
) {
|
|
46
|
+
return await this.__databaseModel.__updateMany(query, ...set);
|
|
47
|
+
},
|
|
48
|
+
async __updateOne(
|
|
49
|
+
this: DatabaseService,
|
|
50
|
+
query: QueryOf<any>,
|
|
51
|
+
...set: [DocumentUpdateInput, DocumentTouchOptions?]
|
|
52
|
+
) {
|
|
53
|
+
return await this.__databaseModel.__updateOne(query, ...set);
|
|
46
54
|
},
|
|
47
55
|
};
|
|
48
56
|
}
|
package/service/agent.service.ts
CHANGED
|
@@ -35,13 +35,16 @@ export class AgentService extends serve("agent" as const, ({ plug }) => ({
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Prepended to the app's instructions here, not per adaptor, where forgetting it would be silent. The batching and
|
|
38
|
-
* no-re-read sentences were measured to cut turns against a provider; do not shorten them.
|
|
38
|
+
* no-re-read sentences were measured to cut turns against a provider; do not shorten them. The navigation sentence
|
|
39
|
+
* carves the batching rule: a call batched behind `navigate` was written against the screen being left.
|
|
39
40
|
*/
|
|
40
41
|
static readonly preamble = [
|
|
41
42
|
"You are an in-page assistant. Use the published tools to read and drive the screen the user is looking at.",
|
|
42
43
|
"Issue every tool call that does not need another call's result in the same turn, rather than one call per turn.",
|
|
43
44
|
"Prefer the tool that does the whole job in one call, such as a form's fill tool over one call per field.",
|
|
44
45
|
"A tool result already reports what it changed, so never call a read afterwards to confirm work you have just done — answer the user from the results you already have.",
|
|
46
|
+
"Tools belong to the screen on display: navigate and goBack replace the screen's own tools, and the new screen's are listed only on your next turn, so end the turn at a navigation and call them once they are listed.",
|
|
47
|
+
"Call a tool only by a name listed to you — never compose or guess one.",
|
|
45
48
|
].join(" ");
|
|
46
49
|
|
|
47
50
|
static instructed(request: LlmTurnRequest): LlmTurnRequest {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { adapt } from "../adapt";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type LlmAccepts,
|
|
4
|
+
type LlmAdaptor,
|
|
5
|
+
type LlmLimits,
|
|
6
|
+
type LlmOption,
|
|
7
|
+
type LlmTurnAnswer,
|
|
8
|
+
type LlmTurnRequest,
|
|
9
|
+
llmProviderOf,
|
|
10
|
+
} from "./llm.adaptor";
|
|
3
11
|
import { LlmOverflow } from "./llmOverflow";
|
|
4
12
|
import { type OpenaiAnswer, OpenaiDialect } from "./openaiDialect";
|
|
5
13
|
|
|
@@ -24,10 +32,18 @@ export class OpenaiLlm
|
|
|
24
32
|
return this.llmOption.contextWindow ? { window: this.llmOption.contextWindow } : {};
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
/** A named host is text-only until `setLlm({ accepts })` says otherwise: undecodable bytes fail the whole turn. */
|
|
28
35
|
get accepts(): LlmAccepts | undefined {
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
return OpenaiLlm.acceptsOf(this.llmOption);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A named host is text-only until `setLlm({ accepts })` says otherwise: undecodable bytes fail the whole turn.
|
|
41
|
+
* OpenAI's own endpoint named outright is still OpenAI rather than a gateway. Static, so an app's own
|
|
42
|
+
* chat-completions adaptor claims what this one does instead of forking the rule.
|
|
43
|
+
*/
|
|
44
|
+
static acceptsOf({ accepts, host }: LlmOption): LlmAccepts | undefined {
|
|
45
|
+
if (accepts) return accepts;
|
|
46
|
+
return !host || llmProviderOf(host) === llmProviderOf(OpenaiLlm.defaultHost) ? { image: true } : undefined;
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
async chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null> {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type DocumentQuery,
|
|
10
10
|
type DocumentSchema,
|
|
11
11
|
type DocumentScopeOptions,
|
|
12
|
+
type DocumentTouchOptions,
|
|
12
13
|
type DocumentUpdate,
|
|
13
14
|
type DocumentUpdateInput,
|
|
14
15
|
type DocumentUpdateNode,
|
|
@@ -471,7 +472,7 @@ export class SqlDocumentStore {
|
|
|
471
472
|
operation: string,
|
|
472
473
|
) {
|
|
473
474
|
const resolved = resolveDocumentUpdate(update);
|
|
474
|
-
const { assignments, params } = this.compiledUpdate(resolved);
|
|
475
|
+
const { assignments, params } = this.compiledUpdate(resolved, options);
|
|
475
476
|
const { where, params: whereParams } = this.writeQuery(query, operation, options);
|
|
476
477
|
const subquery = `SELECT ${quoteIdent("id")} FROM ${quoteIdent(this.table)} WHERE ${where} ORDER BY ${this.compiler.orderBy()} LIMIT 1`;
|
|
477
478
|
|
|
@@ -490,17 +491,21 @@ export class SqlDocumentStore {
|
|
|
490
491
|
return { acknowledged: true, matchedCount: 0, modifiedCount: 1, upsertedId: inserted.id };
|
|
491
492
|
}
|
|
492
493
|
|
|
493
|
-
async updateManyByQuery(
|
|
494
|
+
async updateManyByQuery(
|
|
495
|
+
query: DocumentQuery,
|
|
496
|
+
update: DocumentUpdateInput,
|
|
497
|
+
options: DocumentScopeOptions & DocumentTouchOptions = {},
|
|
498
|
+
) {
|
|
494
499
|
return await this.#updateMany(query, update, options, "updateManyByQuery");
|
|
495
500
|
}
|
|
496
501
|
|
|
497
502
|
async #updateMany(
|
|
498
503
|
query: DocumentQuery,
|
|
499
504
|
update: DocumentUpdateInput,
|
|
500
|
-
options: DocumentScopeOptions,
|
|
505
|
+
options: DocumentScopeOptions & DocumentTouchOptions,
|
|
501
506
|
operation: string,
|
|
502
507
|
) {
|
|
503
|
-
const { assignments, params } = this.compiledUpdate(resolveDocumentUpdate(update));
|
|
508
|
+
const { assignments, params } = this.compiledUpdate(resolveDocumentUpdate(update), options);
|
|
504
509
|
const { where, params: whereParams } = this.writeQuery(query, operation, options);
|
|
505
510
|
const sql = `UPDATE ${quoteIdent(this.table)} SET ${assignments.join(", ")} WHERE ${where}`;
|
|
506
511
|
const changes = this.dialect.affectedRows(
|
|
@@ -559,14 +564,14 @@ export class SqlDocumentStore {
|
|
|
559
564
|
return false;
|
|
560
565
|
}
|
|
561
566
|
|
|
562
|
-
private compiledUpdate(update: DocumentUpdate) {
|
|
567
|
+
private compiledUpdate(update: DocumentUpdate, { touch = true }: DocumentTouchOptions = {}) {
|
|
563
568
|
for (const raw of Object.values(update))
|
|
564
569
|
assertStorableJson(jsonStr(isDocumentUpdateNode(raw) ? raw.value : raw), this.table);
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
};
|
|
570
|
+
const { assignments, params } = this.updateCompiler.compile(update);
|
|
571
|
+
const updatedAt = quoteIdent("updatedAt");
|
|
572
|
+
if (touch) return { assignments: [`${updatedAt} = ?`, ...assignments], params: [Date.now(), ...params] };
|
|
573
|
+
|
|
574
|
+
return { assignments: assignments.length ? assignments : [`${updatedAt} = ${updatedAt}`], params };
|
|
570
575
|
}
|
|
571
576
|
|
|
572
577
|
async bulkWrite(
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
DocumentQueryNode,
|
|
7
7
|
DocumentSchema,
|
|
8
8
|
DocumentScopeOptions,
|
|
9
|
+
DocumentTouchOptions,
|
|
9
10
|
DocumentUpdateInput,
|
|
10
11
|
DocumentUpdateOperator,
|
|
11
12
|
DocumentUpdateOptions,
|
|
@@ -66,7 +67,7 @@ export interface DocumentStore {
|
|
|
66
67
|
updateManyByQuery(
|
|
67
68
|
query: DocumentQuery,
|
|
68
69
|
update: DocumentUpdateInput,
|
|
69
|
-
options?: DocumentScopeOptions,
|
|
70
|
+
options?: DocumentScopeOptions & DocumentTouchOptions,
|
|
70
71
|
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
|
|
71
72
|
removeManyByQuery(
|
|
72
73
|
query: DocumentQuery,
|
package/service/serviceModule.ts
CHANGED
|
@@ -207,11 +207,11 @@ export class ServiceModel<
|
|
|
207
207
|
},
|
|
208
208
|
[`update${key}`]: function (this: DatabaseService, ...args: any): UpdateChain {
|
|
209
209
|
const query = queryOf(args);
|
|
210
|
-
return { set: (
|
|
210
|
+
return { set: (...set) => this.__updateMany(query, ...set) };
|
|
211
211
|
},
|
|
212
212
|
[`updateOne${key}`]: function (this: DatabaseService, ...args: any): UpdateChain {
|
|
213
213
|
const query = queryOf(args);
|
|
214
|
-
return { set: (
|
|
214
|
+
return { set: (...set) => this.__updateOne(query, ...set) };
|
|
215
215
|
},
|
|
216
216
|
};
|
|
217
217
|
}
|
package/service/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { QueryOf } from "akanjs/constant";
|
|
|
4
4
|
import type {
|
|
5
5
|
DatabaseModel,
|
|
6
6
|
DataInputOf,
|
|
7
|
+
DocumentTouchOptions,
|
|
7
8
|
DocumentUpdateInput,
|
|
8
9
|
FilterInstance,
|
|
9
10
|
FindQueryOption,
|
|
@@ -106,8 +107,16 @@ export type DatabaseService<
|
|
|
106
107
|
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
107
108
|
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
108
109
|
__purgeMany: (query: _QueryOfDoc) => Promise<number>;
|
|
109
|
-
__updateMany: (
|
|
110
|
-
|
|
110
|
+
__updateMany: (
|
|
111
|
+
query: _QueryOfDoc,
|
|
112
|
+
update: DocumentUpdateInput<Doc>,
|
|
113
|
+
options?: DocumentTouchOptions,
|
|
114
|
+
) => Promise<UpdateResult>;
|
|
115
|
+
__updateOne: (
|
|
116
|
+
query: _QueryOfDoc,
|
|
117
|
+
update: DocumentUpdateInput<Doc>,
|
|
118
|
+
options?: DocumentTouchOptions,
|
|
119
|
+
) => Promise<UpdateResult>;
|
|
111
120
|
__list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
112
121
|
__listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
113
122
|
__find(query?: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { router } from "akanjs/client";
|
|
2
1
|
import type { ToolActivity } from "../../vendor/use-agentic";
|
|
3
2
|
import { AgentCursor } from "./AgentCursor";
|
|
4
3
|
import { ScreenFlash } from "./ScreenFlash";
|
|
@@ -108,28 +107,17 @@ export class AgentVisual {
|
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
static #linkTo(path: unknown): HTMLElement[] {
|
|
111
|
-
const wanted =
|
|
110
|
+
const wanted = ScreenTarget.routeOf(typeof path === "string" ? path : "");
|
|
112
111
|
if (!wanted || !document.body) return [];
|
|
113
112
|
const links = [...document.body.querySelectorAll<HTMLAnchorElement>("a[href]")].filter(
|
|
114
113
|
(link) =>
|
|
115
|
-
|
|
114
|
+
ScreenTarget.routeOf(link.getAttribute("href") ?? "") === wanted &&
|
|
116
115
|
ScreenTarget.visible(link) &&
|
|
117
116
|
ScreenFlash.inView(link),
|
|
118
117
|
);
|
|
119
118
|
return AgentVisual.#only(links);
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
static #routeOf(href: string) {
|
|
123
|
-
if (!href || href.startsWith("#")) return "";
|
|
124
|
-
try {
|
|
125
|
-
const url = new URL(href, window.location.href);
|
|
126
|
-
if (url.origin !== window.location.origin) return "";
|
|
127
|
-
return router.routeOf(url.pathname);
|
|
128
|
-
} catch {
|
|
129
|
-
return "";
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
121
|
static #navigating(name: string) {
|
|
134
122
|
return name.slice(name.lastIndexOf(".") + 1) === "navigate";
|
|
135
123
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { router } from "akanjs/client";
|
|
1
2
|
import { ScreenReader } from "./ScreenReader";
|
|
2
3
|
|
|
3
4
|
const containerAttrs = ["data-agent-zone", "data-agent-scope", "data-agent-skip"] as const;
|
|
@@ -75,6 +76,30 @@ export class ScreenTarget {
|
|
|
75
76
|
);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
static linkedRoutes(root?: HTMLElement | null): string[] {
|
|
80
|
+
const scope = root ?? ScreenTarget.#body();
|
|
81
|
+
if (!scope) return [];
|
|
82
|
+
const routes = new Set<string>();
|
|
83
|
+
for (const link of scope.querySelectorAll<HTMLAnchorElement>("a[href]")) {
|
|
84
|
+
const route = ScreenTarget.visible(link) ? ScreenTarget.routeOf(link.getAttribute("href") ?? "") : "";
|
|
85
|
+
if (route) routes.add(route);
|
|
86
|
+
if (routes.size >= nameCap) break;
|
|
87
|
+
}
|
|
88
|
+
return [...routes];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Through `router.routeOf`: an href carries locale and base-path segments a tool argument lacks. */
|
|
92
|
+
static routeOf(href: string) {
|
|
93
|
+
if (!href || href.startsWith("#")) return "";
|
|
94
|
+
try {
|
|
95
|
+
const url = new URL(href, window.location.href);
|
|
96
|
+
if (url.origin !== window.location.origin) return "";
|
|
97
|
+
return router.routeOf(url.pathname);
|
|
98
|
+
} catch {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
static #names(attrs: readonly string[], root?: HTMLElement | null): string[] {
|
|
79
104
|
const scope = root ?? ScreenTarget.#body();
|
|
80
105
|
if (!scope) return [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { router } from "akanjs/client";
|
|
1
|
+
import { csrLocation, router } from "akanjs/client";
|
|
2
2
|
import { AgenticSurface, type SurfaceSource, type ToolEntry, ToolReturn } from "../../vendor/use-agentic";
|
|
3
3
|
import { AgentBridge } from "./AgentBridge";
|
|
4
4
|
import { ScreenFlash } from "./ScreenFlash";
|
|
@@ -58,17 +58,38 @@ export class StoreSurfaceSource implements SurfaceSource {
|
|
|
58
58
|
router.push(path);
|
|
59
59
|
await router.navigation();
|
|
60
60
|
} catch {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`There is no route at ${path}, so the page did not move. Call readScreen — it prints each link on this screen with its own path — and navigate to one of those.`,
|
|
63
|
-
);
|
|
61
|
+
throw new Error(`There is no route at ${path}, so the page did not move. ${StoreSurfaceSource.#linked()}`);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
await ScreenSettle.wait({ appearMs: 800, timeoutMs: 5000 });
|
|
67
|
-
|
|
65
|
+
const landed = StoreSurfaceSource.#redirected(path);
|
|
66
|
+
return `${landed ? `Asked for ${path}, but the app sent the page to ${landed} instead.` : `Now on ${path}.`} ${StoreSurfaceSource.#arrived}`;
|
|
68
67
|
},
|
|
69
68
|
};
|
|
70
69
|
}
|
|
71
70
|
|
|
71
|
+
static readonly #arrived =
|
|
72
|
+
"This screen's own tools and state replace the previous screen's and are listed on your next turn — call them then, by the names listed. Call readScreen to see the page.";
|
|
73
|
+
|
|
74
|
+
static #linked() {
|
|
75
|
+
const routes = ScreenTarget.linkedRoutes();
|
|
76
|
+
return routes.length
|
|
77
|
+
? `Paths this screen links to: ${routes.join(", ")}. Navigate to one of those rather than composing a path.`
|
|
78
|
+
: "Call readScreen — it prints each link on this screen with its own path — and navigate to one of those.";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A redirect or the app's not-found page moves the page somewhere other than the path it was sent to. */
|
|
82
|
+
static #redirected(path: string): string | null {
|
|
83
|
+
const trimmed = (route: string) => (route.length > 1 ? route.replace(/\/+$/, "") : route);
|
|
84
|
+
try {
|
|
85
|
+
const landed = router.routeOf(csrLocation.pathname);
|
|
86
|
+
const wanted = router.routeOf(new URL(path, "http://route.invalid").pathname);
|
|
87
|
+
return trimmed(landed) === trimmed(wanted) ? null : landed;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
static #goBack(): ToolEntry {
|
|
73
94
|
return {
|
|
74
95
|
name: "goBack",
|
|
@@ -79,7 +100,7 @@ export class StoreSurfaceSource implements SurfaceSource {
|
|
|
79
100
|
run: async () => {
|
|
80
101
|
router.back();
|
|
81
102
|
await ScreenSettle.wait({ appearMs: 800, timeoutMs: 5000 });
|
|
82
|
-
return `Back on ${router.getPath()}.
|
|
103
|
+
return `Back on ${router.getPath()}. ${StoreSurfaceSource.#arrived}`;
|
|
83
104
|
},
|
|
84
105
|
};
|
|
85
106
|
}
|
package/types/client/router.d.ts
CHANGED
|
@@ -87,6 +87,7 @@ declare class Router {
|
|
|
87
87
|
page: number;
|
|
88
88
|
}): void;
|
|
89
89
|
resolveDeepLinkStack(href: string): string[];
|
|
90
|
+
/** Before init the link waits for it, and the answer is `true`: the manifest that could refuse it is not loaded yet. */
|
|
90
91
|
enterDeepLink(href: string, options?: DeepLinkOptions): boolean;
|
|
91
92
|
push(href: string, routeOptions?: RouteOptions): never;
|
|
92
93
|
replace(href: string, routeOptions?: RouteOptions): never;
|
|
@@ -3,7 +3,7 @@ import type { Logger } from "akanjs/common";
|
|
|
3
3
|
import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
4
4
|
import type { CacheAdaptor, CacheSetOptions, CacheValue } from "akanjs/service";
|
|
5
5
|
import type { DataLoader } from "./dataLoader.d.ts";
|
|
6
|
-
import type { DocumentUpdateInput } from "./documentQuery.d.ts";
|
|
6
|
+
import type { DocumentTouchOptions, DocumentUpdateInput } from "./documentQuery.d.ts";
|
|
7
7
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta.d.ts";
|
|
8
8
|
import type { Mdl, SaveEventListener, SaveEventType, UpdateResult } from "./into.d.ts";
|
|
9
9
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types.d.ts";
|
|
@@ -29,7 +29,7 @@ export declare class CacheDatabase<_T = unknown> {
|
|
|
29
29
|
}
|
|
30
30
|
/** What `update<Filter>` returns: the patch cannot trail a filter's optional args, so it lands on `.set()`. */
|
|
31
31
|
export interface UpdateChain<Doc = any> {
|
|
32
|
-
set(update: DocumentUpdateInput<Doc
|
|
32
|
+
set(update: DocumentUpdateInput<Doc>, options?: DocumentTouchOptions): Promise<UpdateResult>;
|
|
33
33
|
}
|
|
34
34
|
type QueryMethodOfKey<CapitalizedK extends string, Doc, Insight, _Args extends any[], _ListArgs extends any[], _FindArgs extends any[], _QueryOfDoc = QueryOf<Doc>> = {
|
|
35
35
|
[K in `list${CapitalizedK}`]: (...args: _ListArgs) => Promise<Doc[]>;
|
|
@@ -86,8 +86,8 @@ export type DatabaseInstanceWithQuerySort<T extends string = string, Input = any
|
|
|
86
86
|
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
87
87
|
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
88
88
|
__purgeMany: (query: _QueryOfDoc) => Promise<number>;
|
|
89
|
-
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc
|
|
90
|
-
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc
|
|
89
|
+
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>, options?: DocumentTouchOptions) => Promise<UpdateResult>;
|
|
90
|
+
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>, options?: DocumentTouchOptions) => Promise<UpdateResult>;
|
|
91
91
|
__list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
92
92
|
__listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
93
93
|
__find(query: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|
|
@@ -55,7 +55,11 @@ export type DocumentUpdate<T = any> = {
|
|
|
55
55
|
export interface DocumentScopeOptions {
|
|
56
56
|
withRemoved?: boolean;
|
|
57
57
|
}
|
|
58
|
-
export interface
|
|
58
|
+
export interface DocumentTouchOptions {
|
|
59
|
+
/** `false` leaves `updatedAt` as stored, for bookkeeping that is no edit: a lease heartbeat, a mirrored snapshot. */
|
|
60
|
+
touch?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface DocumentUpdateOptions extends DocumentScopeOptions, DocumentTouchOptions {
|
|
59
63
|
upsert?: boolean;
|
|
60
64
|
}
|
|
61
65
|
export declare const createDocumentUpdateHelper: () => {
|
package/types/document/into.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
|
3
3
|
import type { FilterCls, FilterQueryOf, FilterSortOf } from ".";
|
|
4
4
|
import type { CacheDatabase, QueryMethodPart } from "./database.d.ts";
|
|
5
5
|
import type { DataLoader } from "./dataLoader.d.ts";
|
|
6
|
-
import type { DocumentQuery, DocumentScopeOptions, DocumentUpdateInput, DocumentUpdateOptions } from "./documentQuery.d.ts";
|
|
6
|
+
import type { DocumentQuery, DocumentScopeOptions, DocumentTouchOptions, DocumentUpdateInput, DocumentUpdateOptions } from "./documentQuery.d.ts";
|
|
7
7
|
import { type LoaderBuilder, type ModelCls } from "./loaderInfo.d.ts";
|
|
8
8
|
import type { DocumentProjection } from "./types.d.ts";
|
|
9
9
|
export type CRUDEventType = "create" | "update" | "remove";
|
|
@@ -52,7 +52,7 @@ export type Mdl<Doc, Raw, _RawDoc = DocumentModel<Raw>, _RawQuery extends Docume
|
|
|
52
52
|
count(query: _RawQuery, options?: DocumentScopeOptions): Promise<number>;
|
|
53
53
|
exists(query: _RawQuery, options?: DocumentScopeOptions): Promise<string | null>;
|
|
54
54
|
updateOne(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>, options?: DocumentUpdateOptions): Promise<UpdateResult>;
|
|
55
|
-
updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>, options?: DocumentScopeOptions): Promise<UpdateResult>;
|
|
55
|
+
updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>, options?: DocumentScopeOptions & DocumentTouchOptions): Promise<UpdateResult>;
|
|
56
56
|
removeOne(query: _RawQuery): Promise<UpdateResult>;
|
|
57
57
|
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
|
58
58
|
/** Deletes every row the query matches, removed ones too, and answers how many: no hook, cascade or undo. */
|
|
@@ -13,7 +13,8 @@ export declare class AgentService extends AgentService_base {
|
|
|
13
13
|
}>;
|
|
14
14
|
/**
|
|
15
15
|
* Prepended to the app's instructions here, not per adaptor, where forgetting it would be silent. The batching and
|
|
16
|
-
* no-re-read sentences were measured to cut turns against a provider; do not shorten them.
|
|
16
|
+
* no-re-read sentences were measured to cut turns against a provider; do not shorten them. The navigation sentence
|
|
17
|
+
* carves the batching rule: a call batched behind `navigate` was written against the screen being left.
|
|
17
18
|
*/
|
|
18
19
|
static readonly preamble: string;
|
|
19
20
|
static instructed(request: LlmTurnRequest): LlmTurnRequest;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LlmAccepts, type LlmAdaptor, type LlmLimits, type LlmOption, type LlmTurnAnswer, type LlmTurnRequest } from "./llm.adaptor";
|
|
2
2
|
declare const OpenaiLlm_base: import("..").AdaptorCls<{}, {
|
|
3
3
|
llmOption: import("..").InjectInfo<"use", LlmOption, never, never>;
|
|
4
4
|
}>;
|
|
@@ -11,8 +11,13 @@ export declare class OpenaiLlm extends OpenaiLlm_base implements LlmAdaptor {
|
|
|
11
11
|
static readonly defaultHost = "https://api.openai.com/v1";
|
|
12
12
|
/** No answer ceiling: the dialect sends none, so the provider's own default is the one that applies. */
|
|
13
13
|
get limits(): LlmLimits;
|
|
14
|
-
/** A named host is text-only until `setLlm({ accepts })` says otherwise: undecodable bytes fail the whole turn. */
|
|
15
14
|
get accepts(): LlmAccepts | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* A named host is text-only until `setLlm({ accepts })` says otherwise: undecodable bytes fail the whole turn.
|
|
17
|
+
* OpenAI's own endpoint named outright is still OpenAI rather than a gateway. Static, so an app's own
|
|
18
|
+
* chat-completions adaptor claims what this one does instead of forking the rule.
|
|
19
|
+
*/
|
|
20
|
+
static acceptsOf({ accepts, host }: LlmOption): LlmAccepts | undefined;
|
|
16
21
|
chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null>;
|
|
17
22
|
static refusal(host: string, response: Response): Promise<Error>;
|
|
18
23
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ConstantModel } from "akanjs/constant";
|
|
2
|
-
import { type DatabaseModel, type DocumentQuery, type DocumentSchema, type DocumentScopeOptions, type DocumentUpdateInput, type DocumentUpdateOptions } from "akanjs/document";
|
|
2
|
+
import { type DatabaseModel, type DocumentQuery, type DocumentSchema, type DocumentScopeOptions, type DocumentTouchOptions, type DocumentUpdateInput, type DocumentUpdateOptions } from "akanjs/document";
|
|
3
3
|
import { QueryCompiler } from "./QueryCompiler.d.ts";
|
|
4
4
|
import { type DocumentDatabaseOwner, type DocumentRecord, type FindIdOptions, type FindManyOptions, type FindOneOptions, type SqlDialect, type TransferRow, type UpsertByIdOptions, type WriteHookOptions } from "./types.d.ts";
|
|
5
5
|
import { UpdateCompiler } from "./UpdateCompiler.d.ts";
|
|
@@ -34,7 +34,7 @@ export declare class SqlDocumentStore {
|
|
|
34
34
|
modifiedCount: number;
|
|
35
35
|
upsertedId: any;
|
|
36
36
|
}>;
|
|
37
|
-
updateManyByQuery(query: DocumentQuery, update: DocumentUpdateInput, options?: DocumentScopeOptions): Promise<{
|
|
37
|
+
updateManyByQuery(query: DocumentQuery, update: DocumentUpdateInput, options?: DocumentScopeOptions & DocumentTouchOptions): Promise<{
|
|
38
38
|
acknowledged: boolean;
|
|
39
39
|
matchedCount: number;
|
|
40
40
|
modifiedCount: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import type { ConstantModel } from "akanjs/constant";
|
|
3
|
-
import type { DatabaseModel, DocumentQuery, DocumentQueryNode, DocumentSchema, DocumentScopeOptions, DocumentUpdateInput, DocumentUpdateOperator, DocumentUpdateOptions } from "akanjs/document";
|
|
3
|
+
import type { DatabaseModel, DocumentQuery, DocumentQueryNode, DocumentSchema, DocumentScopeOptions, DocumentTouchOptions, DocumentUpdateInput, DocumentUpdateOperator, DocumentUpdateOptions } from "akanjs/document";
|
|
4
4
|
import type { SearchIndex } from "../searchIndex.d.ts";
|
|
5
5
|
import type { SqlDocumentStore } from "./SqlDocumentStore.d.ts";
|
|
6
6
|
export interface SqliteDatabaseConfig {
|
|
@@ -52,7 +52,7 @@ export interface DocumentStore {
|
|
|
52
52
|
modifiedCount: number;
|
|
53
53
|
upsertedId: string | null;
|
|
54
54
|
}>;
|
|
55
|
-
updateManyByQuery(query: DocumentQuery, update: DocumentUpdateInput, options?: DocumentScopeOptions): Promise<{
|
|
55
|
+
updateManyByQuery(query: DocumentQuery, update: DocumentUpdateInput, options?: DocumentScopeOptions & DocumentTouchOptions): Promise<{
|
|
56
56
|
acknowledged: boolean;
|
|
57
57
|
matchedCount: number;
|
|
58
58
|
modifiedCount: number;
|
package/types/service/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Cls, MergeAllTypes, PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import type { Logger } from "akanjs/common";
|
|
3
3
|
import type { QueryOf } from "akanjs/constant";
|
|
4
|
-
import type { DatabaseModel, DataInputOf, DocumentUpdateInput, FilterInstance, FindQueryOption, GetDocObject, ListQueryOption, QueryMethodPart, SaveEventListener, SaveEventType, UpdateResult } from "akanjs/document";
|
|
4
|
+
import type { DatabaseModel, DataInputOf, DocumentTouchOptions, DocumentUpdateInput, FilterInstance, FindQueryOption, GetDocObject, ListQueryOption, QueryMethodPart, SaveEventListener, SaveEventType, UpdateResult } from "akanjs/document";
|
|
5
5
|
type ServiceMixinOmitKey = "onInit" | "onDestroy" | "_libsOnInit" | "_libsOnDestroy" | "_preCreate" | "_postCreate" | "_preUpdate" | "_postUpdate" | "_preRemove" | "_postRemove" | "_libsPreCreate" | "_libsPostCreate" | "_libsPreUpdate" | "_libsPostUpdate" | "_libsPreRemove" | "_libsPostRemove";
|
|
6
6
|
type DatabaseQueryMethods<Query, Sort, Obj, Doc, Insight, FindQueryOption, ListQueryOption, DocQuery> = QueryMethodPart<Query, Sort, Obj, Doc, Insight, FindQueryOption, ListQueryOption, DocQuery>;
|
|
7
7
|
type DocumentLike = {
|
|
@@ -34,8 +34,8 @@ export type DatabaseService<T extends string = string, Input = any, Doc = any, O
|
|
|
34
34
|
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
35
35
|
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
36
36
|
__purgeMany: (query: _QueryOfDoc) => Promise<number>;
|
|
37
|
-
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc
|
|
38
|
-
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc
|
|
37
|
+
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>, options?: DocumentTouchOptions) => Promise<UpdateResult>;
|
|
38
|
+
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>, options?: DocumentTouchOptions) => Promise<UpdateResult>;
|
|
39
39
|
__list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
40
40
|
__listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
41
41
|
__find(query?: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|
|
@@ -10,5 +10,8 @@ export declare class ScreenTarget {
|
|
|
10
10
|
static containerNames(root?: HTMLElement | null): string[];
|
|
11
11
|
static anchorNames(root?: HTMLElement | null): string[];
|
|
12
12
|
static targetNames(root?: HTMLElement | null): string[];
|
|
13
|
+
static linkedRoutes(root?: HTMLElement | null): string[];
|
|
14
|
+
/** Through `router.routeOf`: an href carries locale and base-path segments a tool argument lacks. */
|
|
15
|
+
static routeOf(href: string): string;
|
|
13
16
|
static visible(el: HTMLElement): boolean;
|
|
14
17
|
}
|
|
@@ -192,6 +192,8 @@ export class AgentSession {
|
|
|
192
192
|
#limits: TurnLimits = {};
|
|
193
193
|
/** What the tools, the screen context and the instructions cost the last request, for a host's gauge. */
|
|
194
194
|
#overhead = 0;
|
|
195
|
+
/** The tool names the model was handed on the turn now running — what its calls were written against. */
|
|
196
|
+
#listed: readonly string[] = [];
|
|
195
197
|
|
|
196
198
|
constructor(surface: SurfaceView, runner: AgentRunner, options: AgentSessionOptions = {}) {
|
|
197
199
|
this.#surface = surface;
|
|
@@ -211,7 +213,7 @@ export class AgentSession {
|
|
|
211
213
|
fallback: async (call, signal) =>
|
|
212
214
|
call.name === AgentSession.askUserTool.name
|
|
213
215
|
? await this.#ask(call, signal)
|
|
214
|
-
: { id: call.id, name: call.name, error:
|
|
216
|
+
: { id: call.id, name: call.name, error: this.#unknown(call.name) },
|
|
215
217
|
});
|
|
216
218
|
this.#history = options.history;
|
|
217
219
|
this.#onCompact = options.onCompact;
|
|
@@ -746,6 +748,7 @@ export class AgentSession {
|
|
|
746
748
|
): Promise<{ toolCalls: ToolCallRequest[]; stop: TurnStop; overflow?: { message: string; limit?: number } }> {
|
|
747
749
|
const frame = this.#frame();
|
|
748
750
|
this.#overhead = AgentSession.#tokensOfFrame(frame);
|
|
751
|
+
this.#listed = frame.tools.map((tool) => tool.name);
|
|
749
752
|
const request: RunnerRequest = { messages: Transcript.wire(this.#messages), ...frame, signal };
|
|
750
753
|
this.#append({ role: "assistant" });
|
|
751
754
|
let text = "";
|
|
@@ -780,6 +783,20 @@ export class AgentSession {
|
|
|
780
783
|
return Array.isArray(settled.result) ? settled.result.join(", ") : settled.result;
|
|
781
784
|
}
|
|
782
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Answered with the names that exist: a bare refusal sends a model guessing the next name. A screen that moved
|
|
788
|
+
* since the turn's list was written is said so, since that call was written against the screen it left.
|
|
789
|
+
*/
|
|
790
|
+
#unknown(name: string): string {
|
|
791
|
+
const shown = this.#surface.snapshot().tools.map((tool) => tool.name);
|
|
792
|
+
const now = shown.includes(AgentSession.askUserTool.name) ? shown : [...shown, AgentSession.askUserTool.name];
|
|
793
|
+
const listed = new Set(this.#listed);
|
|
794
|
+
const moved = now.length !== listed.size || now.some((one) => !listed.has(one));
|
|
795
|
+
return moved
|
|
796
|
+
? `Unknown tool: ${name}. The screen changed during this turn, so this call was written against tools that are no longer on it. The tools on screen now are listed with their arguments on your next turn — call one then by its exact name: ${now.join(", ")}.`
|
|
797
|
+
: `Unknown tool: ${name}. Call only a tool listed to you, by its exact name — never a guessed one. Listed now: ${now.join(", ")}.`;
|
|
798
|
+
}
|
|
799
|
+
|
|
783
800
|
/** No surface call and so no diff to report: the only thing this changes is what the model knows. */
|
|
784
801
|
async #ask(call: ToolCallRequest, signal: AbortSignal): Promise<ToolCallResult> {
|
|
785
802
|
const base = { id: call.id, name: call.name };
|