@ixo/oracle-runtime-workers 0.42.0 → 0.43.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -67,10 +67,10 @@
|
|
|
67
67
|
"protobufjs": "^7.6.5",
|
|
68
68
|
"@ixo/topic-protocol": "1.0.0-rc.8",
|
|
69
69
|
"survey-core": "2.3.11",
|
|
70
|
-
"@ixo/common": "1.8.
|
|
70
|
+
"@ixo/common": "1.8.1",
|
|
71
71
|
"@ixo/ixo-transaction": "0.2.0",
|
|
72
|
-
"@ixo/ucan": "2.
|
|
73
|
-
"@ixo/action-interactions": "0.
|
|
72
|
+
"@ixo/ucan": "2.4.0",
|
|
73
|
+
"@ixo/action-interactions": "0.2.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ActionInteractionError,
|
|
3
3
|
canonicalJson,
|
|
4
4
|
interactionValuesDigest,
|
|
5
|
+
NO_ACTION_WINDOW,
|
|
5
6
|
InteractionSchema,
|
|
6
7
|
InteractionReferenceSchema,
|
|
7
8
|
type Interaction,
|
|
@@ -35,6 +36,8 @@ const MANUAL_PREFIX = 'interaction-manual:';
|
|
|
35
36
|
const CHUNK_COUNT_PREFIX = 'interaction-chunks:';
|
|
36
37
|
/** `<id>:<index>` → one chunk of such a record's serialized values. */
|
|
37
38
|
const CHUNK_PREFIX = 'interaction-value:';
|
|
39
|
+
/** `<cap group>:<sha256(sessionId)>:<createdAt>:<id>` → id: the cards one chat created, per cap group. */
|
|
40
|
+
const COUNT_PREFIX = 'interaction-count:';
|
|
38
41
|
/**
|
|
39
42
|
* Replays of edits only need the newest requests: an older retry fails the
|
|
40
43
|
* revision compare-and-swap instead, which never applies an edit twice.
|
|
@@ -45,8 +48,8 @@ const MAX_RECORD_BYTES = 64 * 1024;
|
|
|
45
48
|
/** A domain-write approval carries a file batch; 16 KiB stays free for the record's metadata. */
|
|
46
49
|
const MAX_DOMAIN_WRITE_BYTES = 2 * 1024 * 1024 - 16 * 1024;
|
|
47
50
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
48
|
-
const
|
|
49
|
-
/** How long
|
|
51
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
52
|
+
/** How long a record stays readable after its action window, before retention removes its values. */
|
|
50
53
|
const RETAIN_MS = 30 * DAY_MS;
|
|
51
54
|
/** The newest records one session list returns. */
|
|
52
55
|
const LIST_LIMIT = 50;
|
|
@@ -55,9 +58,10 @@ const LIST_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
55
58
|
/** The newest records one reference list returns. */
|
|
56
59
|
const REFERENCE_LIST_LIMIT = 1000;
|
|
57
60
|
/**
|
|
58
|
-
* Cards one chat may create within
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
+
* Cards one chat may create within a week. Kinds with and without an action
|
|
62
|
+
* window are counted apart, so a chat full of charts can still prepare a
|
|
63
|
+
* claim. A question owner is keyed by its request id, so without a cap a
|
|
64
|
+
* runaway agent could write cards as fast as the rate limit allows.
|
|
61
65
|
*/
|
|
62
66
|
const SESSION_DRAFT_LIMIT = 200;
|
|
63
67
|
/** Records retention handles per prepare, so a prepare never pays for a backlog. */
|
|
@@ -67,6 +71,79 @@ const STORAGE_BATCH = 128;
|
|
|
67
71
|
|
|
68
72
|
type Storage = Pick<DurableObjectStorage, 'get' | 'put' | 'delete' | 'list'>;
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* How long each owner kind lives. The runtime owns this table: a client can
|
|
76
|
+
* never choose a longer life. A new kind, a long-living one included, is one
|
|
77
|
+
* row.
|
|
78
|
+
* - `actionWindowMs`: how long the card can be answered or signed, or `null`
|
|
79
|
+
* when it stays open for as long as its chat (`NO_ACTION_WINDOW`).
|
|
80
|
+
* - `keep`: `chat` keeps the record until its chat is deleted; `window` lets
|
|
81
|
+
* retention remove its values 30 days after the window, keeping what stops
|
|
82
|
+
* a duplicate.
|
|
83
|
+
* - `attachable`: the person can save it to their Library, so it always
|
|
84
|
+
* lives with its chat.
|
|
85
|
+
* - `repeats`: the owner takes another operation after a reported one, when
|
|
86
|
+
* the prepare asks for it (a Flow claim step takes many claims).
|
|
87
|
+
*/
|
|
88
|
+
type CardPolicy = { repeats: boolean } & (
|
|
89
|
+
| { actionWindowMs: number; keep: 'window'; attachable: false }
|
|
90
|
+
| { actionWindowMs: number | null; keep: 'chat'; attachable: boolean }
|
|
91
|
+
);
|
|
92
|
+
const CARD_POLICIES: Record<Interaction['owner']['kind'], CardPolicy> = {
|
|
93
|
+
'flow-claim': {
|
|
94
|
+
actionWindowMs: WEEK_MS,
|
|
95
|
+
keep: 'window',
|
|
96
|
+
attachable: false,
|
|
97
|
+
repeats: true,
|
|
98
|
+
},
|
|
99
|
+
'flow-claim-block': {
|
|
100
|
+
actionWindowMs: WEEK_MS,
|
|
101
|
+
keep: 'window',
|
|
102
|
+
attachable: false,
|
|
103
|
+
repeats: true,
|
|
104
|
+
},
|
|
105
|
+
'flow-bid': {
|
|
106
|
+
actionWindowMs: WEEK_MS,
|
|
107
|
+
keep: 'window',
|
|
108
|
+
attachable: false,
|
|
109
|
+
repeats: false,
|
|
110
|
+
},
|
|
111
|
+
approval: {
|
|
112
|
+
actionWindowMs: WEEK_MS,
|
|
113
|
+
keep: 'window',
|
|
114
|
+
attachable: false,
|
|
115
|
+
repeats: false,
|
|
116
|
+
},
|
|
117
|
+
question: {
|
|
118
|
+
actionWindowMs: null,
|
|
119
|
+
keep: 'chat',
|
|
120
|
+
attachable: false,
|
|
121
|
+
repeats: false,
|
|
122
|
+
},
|
|
123
|
+
artifact: {
|
|
124
|
+
actionWindowMs: null,
|
|
125
|
+
keep: 'chat',
|
|
126
|
+
attachable: true,
|
|
127
|
+
repeats: false,
|
|
128
|
+
},
|
|
129
|
+
task: {
|
|
130
|
+
actionWindowMs: null,
|
|
131
|
+
keep: 'chat',
|
|
132
|
+
attachable: false,
|
|
133
|
+
repeats: false,
|
|
134
|
+
},
|
|
135
|
+
'owner-reference': {
|
|
136
|
+
actionWindowMs: null,
|
|
137
|
+
keep: 'chat',
|
|
138
|
+
attachable: false,
|
|
139
|
+
repeats: false,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const cardPolicy = (owner: Pick<Interaction['owner'], 'kind'>) =>
|
|
143
|
+
CARD_POLICIES[owner.kind];
|
|
144
|
+
const capGroup = (owner: Pick<Interaction['owner'], 'kind'>) =>
|
|
145
|
+
cardPolicy(owner).actionWindowMs === null ? 'no-window' : 'window';
|
|
146
|
+
|
|
70
147
|
async function sha256Hex(text: string): Promise<string> {
|
|
71
148
|
const hash = await crypto.subtle.digest(
|
|
72
149
|
'SHA-256',
|
|
@@ -82,6 +159,9 @@ const sortable = (value: number) =>
|
|
|
82
159
|
async function sessionIndexKey(record: Interaction): Promise<string> {
|
|
83
160
|
return `${SESSION_PREFIX}${await sha256Hex(record.sessionId)}:${sortable(record.createdAt)}:${record.id}`;
|
|
84
161
|
}
|
|
162
|
+
async function countIndexKey(record: Interaction): Promise<string> {
|
|
163
|
+
return `${COUNT_PREFIX}${capGroup(record.owner)}:${await sha256Hex(record.sessionId)}:${sortable(record.createdAt)}:${record.id}`;
|
|
164
|
+
}
|
|
85
165
|
const expiryIndexKey = (record: Interaction) =>
|
|
86
166
|
`${EXPIRY_PREFIX}${sortable(record.expiresAt)}:${record.id}`;
|
|
87
167
|
const utf8Bytes = (text: string) => new TextEncoder().encode(text).byteLength;
|
|
@@ -117,6 +197,13 @@ const unreadableRecord = () =>
|
|
|
117
197
|
'record_unreadable',
|
|
118
198
|
'This Companion draft cannot be read; check the claims list before submitting',
|
|
119
199
|
);
|
|
200
|
+
/** A claim step with a filed claim takes another only at the person's request: a form's claim may still be in flight. */
|
|
201
|
+
const repeatRequired = () =>
|
|
202
|
+
new ActionInteractionError(
|
|
203
|
+
409,
|
|
204
|
+
'repeat_required',
|
|
205
|
+
'This claim step already has a claim in this run, from a card or the form, and it may still be on its way; tell the person, and set repeat only if they ask for another claim',
|
|
206
|
+
);
|
|
120
207
|
type RecentRequest = { requestId: string; fingerprint: string };
|
|
121
208
|
const manualMarkerKey = (ownerKey: string) =>
|
|
122
209
|
`${MANUAL_PREFIX}${ownerKey.slice(ownerKey.indexOf(':') + 1)}`;
|
|
@@ -222,14 +309,18 @@ function extendsReceipt(
|
|
|
222
309
|
* Where an owner's existing record stands for a new card or a manual
|
|
223
310
|
* submission of the same step:
|
|
224
311
|
* - `blocked`: its operation reached the executor (the one-use dispatch key
|
|
225
|
-
* was consumed) or the host reported it observed
|
|
226
|
-
*
|
|
312
|
+
* was consumed) or the host reported it observed, and the kind does not
|
|
313
|
+
* repeat or the outcome is not final. A second submission could duplicate
|
|
314
|
+
* the claim, so nothing replaces it.
|
|
315
|
+
* - `done`: a reported result of a kind that repeats. The record stays as
|
|
316
|
+
* it is; the ordinary form may submit again, and a card only when its
|
|
317
|
+
* prepare asks to repeat.
|
|
227
318
|
* - `live`: an unexpired draft, or a reservation the host is dispatching
|
|
228
319
|
* right now. It belongs to the card that owns it.
|
|
229
320
|
* - `stale`: cancelled, expired, or a reservation that never reached the
|
|
230
321
|
* executor. Safe to supersede: its dispatch check can no longer pass.
|
|
231
322
|
*/
|
|
232
|
-
type OwnerState = 'blocked' | 'live' | 'stale';
|
|
323
|
+
type OwnerState = 'blocked' | 'done' | 'live' | 'stale';
|
|
233
324
|
|
|
234
325
|
/** Personal drafts only. Execution and verified receipts remain owned by Flow. */
|
|
235
326
|
export class ActionInteractionStore {
|
|
@@ -269,20 +360,23 @@ export class ActionInteractionStore {
|
|
|
269
360
|
// that may already have reached the executor; any other draft for
|
|
270
361
|
// this step is superseded, so its own dispatch check can no longer
|
|
271
362
|
// pass and the step is never locked by an abandoned card. The
|
|
272
|
-
// marker stops a card prepared later from filing the same claim
|
|
363
|
+
// marker stops a card prepared later from filing the same claim,
|
|
364
|
+
// unless that prepare asks to repeat a kind that repeats.
|
|
273
365
|
if (id && !record)
|
|
274
366
|
throw new ActionInteractionError(
|
|
275
367
|
409,
|
|
276
368
|
'review_required',
|
|
277
369
|
'This claim step has an unreadable Companion draft',
|
|
278
370
|
);
|
|
279
|
-
|
|
371
|
+
const state = record && (await this.ownerState(tx, record));
|
|
372
|
+
if (state === 'blocked')
|
|
280
373
|
throw new ActionInteractionError(
|
|
281
374
|
409,
|
|
282
375
|
'dispatch_consumed',
|
|
283
376
|
'A Companion submission for this claim step already reached the claim service; check the claims list before submitting again',
|
|
284
377
|
);
|
|
285
|
-
|
|
378
|
+
// A submitted claim stays as it is; another claim is a new one.
|
|
379
|
+
if (record && state !== 'done') await this.supersede(tx, record);
|
|
286
380
|
await tx.put(manualKey, this.now());
|
|
287
381
|
return { allowed: true, managed: false };
|
|
288
382
|
}
|
|
@@ -394,10 +488,11 @@ export class ActionInteractionStore {
|
|
|
394
488
|
|
|
395
489
|
/**
|
|
396
490
|
* Retention for records past `expiresAt + RETAIN_MS`, a bounded batch per
|
|
397
|
-
* call.
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
491
|
+
* call. Only kinds that `keep` their record for their `window` are queued.
|
|
492
|
+
* Superseded or abandoned drafts disappear with their owner key, indexes
|
|
493
|
+
* and request tombstones. A record whose operation reached the executor
|
|
494
|
+
* keeps its owner key, reservation and dispatch tombstone forever (they are
|
|
495
|
+
* what stops a duplicate claim), but loses its draft values.
|
|
401
496
|
*/
|
|
402
497
|
async prune(): Promise<number> {
|
|
403
498
|
const due = await this.storage.list<string>({
|
|
@@ -407,7 +502,13 @@ export class ActionInteractionStore {
|
|
|
407
502
|
});
|
|
408
503
|
if (due.size === 0) return 0;
|
|
409
504
|
await this.storage.transaction(async (tx) => {
|
|
410
|
-
for (const [expiryKey, id] of due)
|
|
505
|
+
for (const [expiryKey, id] of due) {
|
|
506
|
+
const record = await this.recordFrom(tx, id).catch(() => undefined);
|
|
507
|
+
// Queued before its kind lived with its chat: it leaves the queue.
|
|
508
|
+
if (record && cardPolicy(record.owner).keep === 'chat')
|
|
509
|
+
await tx.delete(expiryKey);
|
|
510
|
+
else await this.forget(tx, id, expiryKey);
|
|
511
|
+
}
|
|
411
512
|
});
|
|
412
513
|
return due.size;
|
|
413
514
|
}
|
|
@@ -449,8 +550,9 @@ export class ActionInteractionStore {
|
|
|
449
550
|
|
|
450
551
|
/**
|
|
451
552
|
* Removes a record and everything that points at it, except what stops a
|
|
452
|
-
* duplicate claim: a
|
|
453
|
-
* dispatch tombstone, and only loses its values
|
|
553
|
+
* duplicate claim: a record whose operation reached the executor keeps its
|
|
554
|
+
* owner key, reservation and dispatch tombstone, and only loses its values
|
|
555
|
+
* and user-edit list.
|
|
454
556
|
*/
|
|
455
557
|
private async forget(tx: Storage, id: string, expiryKey?: string) {
|
|
456
558
|
// Only prepare requests are indexed per record (edits keep their
|
|
@@ -485,7 +587,9 @@ export class ActionInteractionStore {
|
|
|
485
587
|
throw error;
|
|
486
588
|
}
|
|
487
589
|
const chunks = record ? await this.chunkKeys(tx, id) : [];
|
|
488
|
-
if (record
|
|
590
|
+
if (record) doomed.push(await countIndexKey(record));
|
|
591
|
+
const state = record && (await this.ownerState(tx, record));
|
|
592
|
+
if (record && (state === 'blocked' || state === 'done')) {
|
|
489
593
|
if (
|
|
490
594
|
Object.keys(record.values).length ||
|
|
491
595
|
record.updatedByUser.length ||
|
|
@@ -521,6 +625,8 @@ export class ActionInteractionStore {
|
|
|
521
625
|
storage: Pick<DurableObjectStorage, 'get'>,
|
|
522
626
|
record: Interaction,
|
|
523
627
|
): Promise<OwnerState> {
|
|
628
|
+
if (record.status === 'observed' && cardPolicy(record.owner).repeats)
|
|
629
|
+
return 'done';
|
|
524
630
|
if (record.status === 'observed' || record.status === 'partial')
|
|
525
631
|
return 'blocked';
|
|
526
632
|
if (record.reservation && (await this.dispatched(storage, record)))
|
|
@@ -744,12 +850,15 @@ export class ActionInteractionStore {
|
|
|
744
850
|
'Approval values must contain exactly one request object',
|
|
745
851
|
);
|
|
746
852
|
const ownerKey = await ownerStorageKey(input.owner, input.sessionId);
|
|
747
|
-
|
|
853
|
+
const policy = cardPolicy(input.owner);
|
|
854
|
+
// Only a kind that repeats can be asked for another operation.
|
|
855
|
+
const repeat = policy.repeats && input.repeat === true;
|
|
856
|
+
const filedByForm =
|
|
748
857
|
(input.owner.kind === 'flow-claim' ||
|
|
749
858
|
input.owner.kind === 'flow-claim-block' ||
|
|
750
859
|
input.owner.kind === 'flow-bid') &&
|
|
751
|
-
(await tx.get<number>(manualMarkerKey(ownerKey))) !== undefined
|
|
752
|
-
)
|
|
860
|
+
(await tx.get<number>(manualMarkerKey(ownerKey))) !== undefined;
|
|
861
|
+
if (filedByForm && !policy.repeats)
|
|
753
862
|
// The ordinary form already passed its dispatch check for this
|
|
754
863
|
// step; a card could file the same claim a second time.
|
|
755
864
|
throw new ActionInteractionError(
|
|
@@ -779,6 +888,7 @@ export class ActionInteractionStore {
|
|
|
779
888
|
const state = await this.ownerState(tx, existing);
|
|
780
889
|
const inFlight = state === 'live' && existing.status === 'reserved';
|
|
781
890
|
if (
|
|
891
|
+
(state === 'done' && !repeat) ||
|
|
782
892
|
state === 'blocked' ||
|
|
783
893
|
inFlight ||
|
|
784
894
|
(state === 'live' && !sameSession)
|
|
@@ -787,6 +897,7 @@ export class ActionInteractionStore {
|
|
|
787
897
|
await remember(tx, existing.id);
|
|
788
898
|
return { interaction: existing, replayed: true };
|
|
789
899
|
}
|
|
900
|
+
if (state === 'done') throw repeatRequired();
|
|
790
901
|
if (state === 'blocked')
|
|
791
902
|
throw new ActionInteractionError(
|
|
792
903
|
409,
|
|
@@ -811,15 +922,19 @@ export class ActionInteractionStore {
|
|
|
811
922
|
}
|
|
812
923
|
// A stale draft, or this chat's own draft for a form whose
|
|
813
924
|
// collection or schema has since changed: it can no longer
|
|
814
|
-
// dispatch, so a fresh draft takes its place.
|
|
815
|
-
|
|
925
|
+
// dispatch, so a fresh draft takes its place. A submitted claim
|
|
926
|
+
// asked to repeat stays as it is.
|
|
927
|
+
if (state !== 'done') await this.supersede(tx, existing);
|
|
816
928
|
}
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
929
|
+
// A claim filed from the form: another one is a new claim.
|
|
930
|
+
if (filedByForm && !repeat) throw repeatRequired();
|
|
931
|
+
// Only cards created within the last week count, apart for kinds with
|
|
932
|
+
// and without an action window: kept records of sent claims and
|
|
933
|
+
// expired cards awaiting retention never lock a chat.
|
|
934
|
+
const counted = `${COUNT_PREFIX}${capGroup(input.owner)}:${await sha256Hex(input.sessionId)}:`;
|
|
820
935
|
const recent = await tx.list<string>({
|
|
821
|
-
prefix:
|
|
822
|
-
start: `${
|
|
936
|
+
prefix: counted,
|
|
937
|
+
start: `${counted}${sortable(this.now() - WEEK_MS)}`,
|
|
823
938
|
limit: SESSION_DRAFT_LIMIT,
|
|
824
939
|
});
|
|
825
940
|
if (recent.size >= SESSION_DRAFT_LIMIT)
|
|
@@ -839,12 +954,19 @@ export class ActionInteractionStore {
|
|
|
839
954
|
updatedByUser: [],
|
|
840
955
|
status: 'draft',
|
|
841
956
|
createdAt: this.now(),
|
|
842
|
-
expiresAt:
|
|
957
|
+
expiresAt:
|
|
958
|
+
policy.actionWindowMs === null
|
|
959
|
+
? NO_ACTION_WINDOW
|
|
960
|
+
: this.now() + policy.actionWindowMs,
|
|
843
961
|
});
|
|
844
962
|
await tx.put({
|
|
845
963
|
[ownerKey]: record.id,
|
|
846
964
|
[await sessionIndexKey(record)]: record.id,
|
|
847
|
-
[
|
|
965
|
+
[await countIndexKey(record)]: record.id,
|
|
966
|
+
// A kind that lives with its chat never waits for retention.
|
|
967
|
+
...(policy.keep === 'window'
|
|
968
|
+
? { [expiryIndexKey(record)]: record.id }
|
|
969
|
+
: {}),
|
|
848
970
|
});
|
|
849
971
|
} else {
|
|
850
972
|
record = await this.readFrom(tx, command.id, command.input.sessionId);
|
|
@@ -33,9 +33,9 @@ import {
|
|
|
33
33
|
const manifest: PluginManifest = {
|
|
34
34
|
title: 'Portal',
|
|
35
35
|
summary:
|
|
36
|
-
"The user's Portal UI: action cards (
|
|
36
|
+
"The user's Portal UI: action cards (everything the person sees, answers, reviews or signs), forms, Topics, domains, pages, navigation.",
|
|
37
37
|
whenToUse: [
|
|
38
|
-
'
|
|
38
|
+
'What the person sees, answers, reviews or signs is an action card: ask with `ask_question_card`; for a bid or claim call `list_action_cards`, then `prepare_action_card`. Only when the Portal declares no card tools, ask with `ask_user_question`.',
|
|
39
39
|
'User asks for an action the Portal FE exposes as a browser tool (declared in `state.browserTools`).',
|
|
40
40
|
"A task needs a browser-side capability the server can't do alone — open a URL in the user's tab, click a Portal button, fill a form.",
|
|
41
41
|
],
|
|
@@ -67,6 +67,13 @@ const HUMAN_DECISION_TOOLS = new Set([
|
|
|
67
67
|
'publish_recipe_release',
|
|
68
68
|
]);
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* How the Agent asks the person something: bound on every turn that declares
|
|
72
|
+
* it, so a question never turns into prose because the Portal capability was
|
|
73
|
+
* not loaded. A chat declares one of them, never both.
|
|
74
|
+
*/
|
|
75
|
+
const ASKING_TOOLS = new Set(['ask_user_question', 'ask_question_card']);
|
|
76
|
+
|
|
70
77
|
/**
|
|
71
78
|
* The Portal's Topic write tool. Its commands can take a while to settle in
|
|
72
79
|
* the browser (several Matrix writes), so it gets a longer window before the
|
|
@@ -213,6 +220,9 @@ export function buildBrowserTool(
|
|
|
213
220
|
// frontend lane, so a parked call (or a question to the user) holds
|
|
214
221
|
// no slot another chat needs.
|
|
215
222
|
surface: 'frontend',
|
|
223
|
+
...(ASKING_TOOLS.has(descriptor.name)
|
|
224
|
+
? { visibility: 'always' as const }
|
|
225
|
+
: {}),
|
|
216
226
|
// A question to the user is a write whatever the client declares:
|
|
217
227
|
// it is never asked twice after a restart once it was sent. Any
|
|
218
228
|
// other tool keeps the effect its declaration names, if any.
|