akanjs 3.0.0-alpha.50 → 3.0.0-alpha.51
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/constant/fieldInfo.ts +32 -0
- package/constant/mask.ts +11 -4
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/akanServer.ts +13 -0
- package/server/mcp/McpDispatcher.ts +42 -2
- package/server/mcp/McpRouter.ts +2 -0
- package/signal/schema/JsonSchemaBuilder.ts +10 -6
- package/store/agent/ScreenReader.ts +36 -6
- package/store/agent/ScreenTarget.ts +13 -7
- package/store/agent/StoreSurfaceSource.ts +1 -1
- package/types/constant/fieldInfo.d.ts +10 -0
- package/types/constant/mask.d.ts +10 -3
- package/types/server/akanServer.d.ts +7 -0
- package/types/server/mcp/McpDispatcher.d.ts +7 -0
- package/types/server/mcp/McpRouter.d.ts +2 -0
- package/types/signal/schema/JsonSchemaBuilder.d.ts +9 -5
- package/types/store/agent/ScreenReader.d.ts +5 -0
- package/types/store/agent/ScreenTarget.d.ts +5 -4
- package/types/ui/Agent/Approval.d.ts +4 -3
- package/types/ui/Agent/Bubble.d.ts +5 -2
- package/types/ui/Agent/Chat.d.ts +17 -1
- package/types/ui/Agent/Composer.d.ts +6 -6
- package/types/ui/Agent/Launcher.d.ts +3 -3
- package/types/ui/Agent/Markdown.d.ts +11 -3
- package/types/ui/Agent/Menu.d.ts +4 -3
- package/types/ui/Agent/Question.d.ts +4 -3
- package/types/ui/Agent/Skip.d.ts +22 -0
- package/types/ui/Agent/index.d.ts +1 -0
- package/types/ui/Agent/markdownBlocks.d.ts +1 -0
- package/types/ui/Agent/useKeyboardInset.d.ts +8 -0
- package/types/ui/UiOverride/context.d.ts +15 -0
- package/types/ui/index.d.ts +13 -0
- package/ui/Agent/Approval.tsx +6 -3
- package/ui/Agent/Bubble.tsx +7 -2
- package/ui/Agent/Chat.tsx +73 -27
- package/ui/Agent/Composer.tsx +27 -10
- package/ui/Agent/Launcher.tsx +5 -2
- package/ui/Agent/Markdown.tsx +32 -15
- package/ui/Agent/Menu.tsx +6 -3
- package/ui/Agent/Question.tsx +6 -3
- package/ui/Agent/Skip.tsx +30 -0
- package/ui/Agent/index.ts +14 -1
- package/ui/Agent/markdownBlocks.ts +7 -5
- package/ui/Agent/useKeyboardInset.ts +26 -0
- package/ui/UiOverride/context.ts +16 -0
- package/ui/index.ts +15 -0
- package/vendor/use-agentic/AgentSession.ts +3 -2
package/constant/fieldInfo.ts
CHANGED
|
@@ -106,6 +106,13 @@ export interface ConstantFieldProps<
|
|
|
106
106
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
107
107
|
text?: TextFieldRole;
|
|
108
108
|
cascade?: CascadeAction;
|
|
109
|
+
/**
|
|
110
|
+
* Renders on the page, never reaches an agent. Stripped wherever a value is masked for an AI caller — the
|
|
111
|
+
* in-page agent's reads and every MCP result — and left untouched everywhere else, so a `File`'s blur
|
|
112
|
+
* placeholder still ships to `<Image>`. Unlike `hidden`/`secret` this is about cost, not secrecy: a field
|
|
113
|
+
* nothing can answer a question with is pure spend on every turn it rides.
|
|
114
|
+
*/
|
|
115
|
+
visual?: boolean;
|
|
109
116
|
meta?: Metadata;
|
|
110
117
|
}
|
|
111
118
|
export const fieldPresets = ["email", "password", "url"] as const;
|
|
@@ -197,6 +204,7 @@ interface ConstantFieldBuildProps<
|
|
|
197
204
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
198
205
|
text?: TextFieldRole;
|
|
199
206
|
cascade?: CascadeAction;
|
|
207
|
+
visual: boolean;
|
|
200
208
|
modelRef: ConstantModelRef;
|
|
201
209
|
arrDepth: number;
|
|
202
210
|
optArrDepth: number;
|
|
@@ -321,6 +329,7 @@ export class ConstantField<
|
|
|
321
329
|
readonly validate?: (value: FieldValue, model: any) => boolean;
|
|
322
330
|
readonly text?: TextFieldRole;
|
|
323
331
|
readonly cascade?: CascadeAction;
|
|
332
|
+
readonly visual: boolean;
|
|
324
333
|
readonly modelRef: ConstantModelRef;
|
|
325
334
|
readonly arrDepth: number;
|
|
326
335
|
readonly optArrDepth: number;
|
|
@@ -353,6 +362,7 @@ export class ConstantField<
|
|
|
353
362
|
this.validate = props.validate;
|
|
354
363
|
this.text = props.text;
|
|
355
364
|
this.cascade = props.cascade;
|
|
365
|
+
this.visual = props.visual;
|
|
356
366
|
this.modelRef = props.modelRef;
|
|
357
367
|
this.arrDepth = props.arrDepth;
|
|
358
368
|
this.optArrDepth = props.optArrDepth;
|
|
@@ -432,6 +442,7 @@ export class ConstantField<
|
|
|
432
442
|
validate: option.validate,
|
|
433
443
|
text: option.text,
|
|
434
444
|
cascade: option.cascade,
|
|
445
|
+
visual: option.visual ?? false,
|
|
435
446
|
modelRef,
|
|
436
447
|
arrDepth: arrDepth,
|
|
437
448
|
optArrDepth: optArrDepth,
|
|
@@ -478,6 +489,7 @@ export class ConstantField<
|
|
|
478
489
|
validate: this.validate,
|
|
479
490
|
text: this.text,
|
|
480
491
|
cascade: this.cascade,
|
|
492
|
+
visual: this.visual,
|
|
481
493
|
modelRef: this.modelRef,
|
|
482
494
|
arrDepth: this.arrDepth,
|
|
483
495
|
optArrDepth: this.optArrDepth,
|
|
@@ -530,6 +542,26 @@ export const field = <
|
|
|
530
542
|
fieldType: "property",
|
|
531
543
|
});
|
|
532
544
|
|
|
545
|
+
/**
|
|
546
|
+
* A stored property the page renders and an agent never sees — `field(value, { visual: true })`, spelled short
|
|
547
|
+
* because the reason to reach for it is always the same one. A blur placeholder, a rendered HTML body, a
|
|
548
|
+
* serialized geometry: real data the screen needs, and hundreds of tokens per record that no question is answered
|
|
549
|
+
* from. It stays a plain `property` everywhere else, so persistence, search, forms and the page response are
|
|
550
|
+
* untouched.
|
|
551
|
+
*/
|
|
552
|
+
field.visual = <
|
|
553
|
+
ExplicitType,
|
|
554
|
+
Value extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>,
|
|
555
|
+
MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never,
|
|
556
|
+
>(
|
|
557
|
+
value: Value,
|
|
558
|
+
option: FieldOption<Value, MapValue> = {},
|
|
559
|
+
) =>
|
|
560
|
+
new FieldInfo<"property", Value, ExplicitType, MapValue>(value, {
|
|
561
|
+
...option,
|
|
562
|
+
fieldType: "property",
|
|
563
|
+
visual: true,
|
|
564
|
+
});
|
|
533
565
|
field.hidden = <
|
|
534
566
|
ExplicitType,
|
|
535
567
|
Value extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>,
|
package/constant/mask.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface MaskField {
|
|
|
15
15
|
fieldType?: string;
|
|
16
16
|
isClass?: boolean;
|
|
17
17
|
modelRef?: MaskModel;
|
|
18
|
+
visual?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const maskFieldsOf = (model: MaskModel): Record<string, MaskField> | null => {
|
|
@@ -22,7 +23,12 @@ export const maskFieldsOf = (model: MaskModel): Record<string, MaskField> | null
|
|
|
22
23
|
return fields && typeof fields === "object" ? (fields as Record<string, MaskField>) : null;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The `hidden` and `secret` field names of `model` that `value` still carries populated.
|
|
28
|
+
*
|
|
29
|
+
* `visual` is deliberately not among them. A refusal here means a value must not be published at all, and a blur
|
|
30
|
+
* placeholder is not a secret — it is merely not worth its tokens, which masking answers by dropping it.
|
|
31
|
+
*/
|
|
26
32
|
export const leakingFieldsOf = (model: MaskModel, value: Record<string, unknown>): string[] => {
|
|
27
33
|
const fields = maskFieldsOf(model);
|
|
28
34
|
if (!fields) return [];
|
|
@@ -32,8 +38,9 @@ export const leakingFieldsOf = (model: MaskModel, value: Record<string, unknown>
|
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
|
-
* Strips what a model marks `hidden` or `
|
|
36
|
-
* happens to still carry.
|
|
41
|
+
* Strips what a model marks `hidden`, `secret`, or `visual`, by the model the caller names rather than by the one
|
|
42
|
+
* the value happens to still carry. The first two are secrecy and the third is cost, but the answer is the same
|
|
43
|
+
* one — leave the field out — and this is the only place every AI-facing read already passes through.
|
|
37
44
|
*
|
|
38
45
|
* That distinction is the whole point. A check that reads the class off the value can only mask what arrives as an
|
|
39
46
|
* instance, so a `{ ...doc }` spread, a `toJSON()`, an `immerify()`, or a round-trip through `JSON.stringify` reaches
|
|
@@ -53,7 +60,7 @@ export const mask = (model: MaskModel, value: unknown): unknown => {
|
|
|
53
60
|
const source = value as Record<string, unknown>;
|
|
54
61
|
const masked: Record<string, unknown> = {};
|
|
55
62
|
for (const [key, field] of Object.entries(fields)) {
|
|
56
|
-
if (field.fieldType === "hidden" || field.fieldType === "secret" || !(key in source)) continue;
|
|
63
|
+
if (field.fieldType === "hidden" || field.fieldType === "secret" || field.visual || !(key in source)) continue;
|
|
57
64
|
masked[key] = field.isClass && field.modelRef ? mask(field.modelRef, source[key]) : source[key];
|
|
58
65
|
}
|
|
59
66
|
return masked;
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/akanServer.ts
CHANGED
|
@@ -80,6 +80,13 @@ export interface McpServerOption {
|
|
|
80
80
|
* the document is built once at boot and cached by clients, and it is read by a model rather than by a person.
|
|
81
81
|
*/
|
|
82
82
|
language?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Whether a structured result also ships as serialized JSON in the text block, `true` by default because that
|
|
85
|
+
* is what the spec asks of a server for clients that predate `structuredContent`. It is also a flat doubling:
|
|
86
|
+
* every model-returning tool sends its whole payload twice, so a deployment whose clients read the structured
|
|
87
|
+
* half turns this off and halves what each of those calls costs the model. `AKAN_MCP_LEGACY_TEXT=false`.
|
|
88
|
+
*/
|
|
89
|
+
legacyTextBlock?: boolean;
|
|
83
90
|
/** OAuth resource-server identity: which issuers a client may authenticate with, and the scopes to demand. */
|
|
84
91
|
auth?: McpAuthOption;
|
|
85
92
|
}
|
|
@@ -622,6 +629,11 @@ export class AkanServer {
|
|
|
622
629
|
return !names.some((name) => process.env[name] === "false" || process.env[name] === "0");
|
|
623
630
|
}
|
|
624
631
|
|
|
632
|
+
/** Named rather than defaulted: an absent env must leave the option unset so a value written in code still wins. */
|
|
633
|
+
static #isEnvOff(...names: string[]) {
|
|
634
|
+
return names.some((name) => process.env[name] === "false" || process.env[name] === "0");
|
|
635
|
+
}
|
|
636
|
+
|
|
625
637
|
/** Both differ per environment, so they belong in env rather than in the app's source alongside the switch. */
|
|
626
638
|
static #mcpAuthFromEnv(): McpAuthOption {
|
|
627
639
|
const authorizationServers = AkanServer.#envList("AKAN_MCP_AUTH_SERVERS");
|
|
@@ -657,6 +669,7 @@ export class AkanServer {
|
|
|
657
669
|
...(allowedOrigins?.length ? { allowedOrigins } : {}),
|
|
658
670
|
...(Number.isInteger(pageSize) && pageSize > 0 ? { pageSize } : {}),
|
|
659
671
|
...(process.env.AKAN_MCP_LANGUAGE ? { language: process.env.AKAN_MCP_LANGUAGE } : {}),
|
|
672
|
+
...(AkanServer.#isEnvOff("AKAN_MCP_LEGACY_TEXT") ? { legacyTextBlock: false } : {}),
|
|
660
673
|
};
|
|
661
674
|
}
|
|
662
675
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BackendEnv, ENDPOINT_META } from "akanjs/base";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
|
+
import { ConstantRegistry, mask } from "akanjs/constant";
|
|
3
4
|
import { DictionaryLookup } from "akanjs/dictionary";
|
|
4
5
|
import { NoDocumentError } from "akanjs/document";
|
|
5
6
|
import type { InjectRegistry, LiveRegistry } from "akanjs/service";
|
|
@@ -42,12 +43,20 @@ interface McpDispatcherProps {
|
|
|
42
43
|
middleware: Map<string, MiddlewareCls>;
|
|
43
44
|
/** The one language error text is resolved in, matching the catalogue the client was handed. */
|
|
44
45
|
language?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a structured result also ships as serialized JSON in the text block. `true` by default, which is what
|
|
48
|
+
* the spec asks for; `false` halves what every model-returning tool costs.
|
|
49
|
+
*/
|
|
50
|
+
legacyTextBlock?: boolean;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/** Executes one MCP tool call or resource read through the ordinary signal pipeline. */
|
|
48
54
|
export class McpDispatcher {
|
|
49
55
|
static readonly logger = new Logger("McpDispatcher");
|
|
50
56
|
|
|
57
|
+
/** What the text block says when the serialized duplicate is off. Read by a model, so English. */
|
|
58
|
+
static readonly structuredNote = "The result is in this call's structuredContent.";
|
|
59
|
+
|
|
51
60
|
readonly #props: McpDispatcherProps;
|
|
52
61
|
#endpoints: Map<string, { endpointInfo: EndpointInfo; endpoint: Endpoint }> | null = null;
|
|
53
62
|
/**
|
|
@@ -68,10 +77,10 @@ export class McpDispatcher {
|
|
|
68
77
|
const found = this.#index().get(exposed.key);
|
|
69
78
|
if (!found) return McpDispatcher.#failure(`Tool "${exposed.key}" is declared but not mounted on this server.`);
|
|
70
79
|
try {
|
|
71
|
-
const value = await this.#exec(exposed.key, found, args, req);
|
|
80
|
+
const value = McpDispatcher.#readable(exposed, await this.#exec(exposed.key, found, args, req));
|
|
72
81
|
const structuredContent = McpDocument.structuredContent(exposed.endpoint, value);
|
|
73
82
|
return {
|
|
74
|
-
content:
|
|
83
|
+
content: this.#content(structuredContent, value),
|
|
75
84
|
...(structuredContent === undefined ? {} : { structuredContent }),
|
|
76
85
|
isError: false,
|
|
77
86
|
};
|
|
@@ -223,6 +232,19 @@ export class McpDispatcher {
|
|
|
223
232
|
return status && status < 500 ? McpErrorCode.invalidParams : McpErrorCode.internal;
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
/**
|
|
236
|
+
* A structured result rides twice by default: once as `structuredContent`, once as the same JSON here, which is
|
|
237
|
+
* what the spec asks of a server for clients that predate the structured field. Every model return therefore
|
|
238
|
+
* costs the model twice what it carries, and `legacyTextBlock: false` is the deployment that has decided its
|
|
239
|
+
* clients read the structured half — the pointer keeps `content` non-empty, since a client that renders
|
|
240
|
+
* `content[0].text` and finds nothing shows an empty answer rather than a missing one.
|
|
241
|
+
*/
|
|
242
|
+
#content(structuredContent: unknown, value: unknown): McpToolResult["content"] {
|
|
243
|
+
if (structuredContent !== undefined && this.#props.legacyTextBlock === false)
|
|
244
|
+
return [{ type: "text", text: McpDispatcher.structuredNote }];
|
|
245
|
+
return [{ type: "text", text: McpDispatcher.#text(structuredContent, value) }];
|
|
246
|
+
}
|
|
247
|
+
|
|
226
248
|
/**
|
|
227
249
|
* The text block every result carries, whether or not a structured one goes with it.
|
|
228
250
|
*
|
|
@@ -239,6 +261,24 @@ export class McpDispatcher {
|
|
|
239
261
|
return JSON.stringify(structuredContent ?? value) ?? "null";
|
|
240
262
|
}
|
|
241
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Strips what the return model marks `visual` — a field the page renders and no question is answered from.
|
|
266
|
+
*
|
|
267
|
+
* Done here rather than in `resolveReturn`, which every ordinary HTTP response also passes through: the point of
|
|
268
|
+
* a `visual` field is that a browser still receives it. MCP results reach an agent and nothing else, so this is
|
|
269
|
+
* where the model's own declaration is honoured, and it is the same `mask` the in-page agent's reads use.
|
|
270
|
+
*/
|
|
271
|
+
static #readable(exposed: McpExposedEndpoint, value: unknown): unknown {
|
|
272
|
+
const { refName, modelType } = exposed.endpoint.returns;
|
|
273
|
+
if (!modelType) return value;
|
|
274
|
+
try {
|
|
275
|
+
return mask(ConstantRegistry.getModelRef(refName, modelType), value);
|
|
276
|
+
} catch {
|
|
277
|
+
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
242
282
|
static #failure(message: string): McpToolResult {
|
|
243
283
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
244
284
|
}
|
package/server/mcp/McpRouter.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface McpRouterProps {
|
|
|
45
45
|
* rarely sees. Defaults to `en`, falling back to the first registered language when the app has no `en`.
|
|
46
46
|
*/
|
|
47
47
|
language?: string;
|
|
48
|
+
/** Whether a structured result also ships as serialized JSON in the text block. Default `true`. */
|
|
49
|
+
legacyTextBlock?: boolean;
|
|
48
50
|
auth?: McpAuthOption;
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -14,11 +14,15 @@ export interface JsonSchemaBuilderOptions {
|
|
|
14
14
|
|
|
15
15
|
export interface JsonSchemaModelOptions {
|
|
16
16
|
/**
|
|
17
|
-
* Drops `hidden` and `
|
|
18
|
-
* them describes a value the caller can never read — and on a model like `user` the names
|
|
19
|
-
* leak: `password`, `accountId`, `phone` published as readable properties of the model. This
|
|
20
|
-
* field the framework blocks on every value path is still visible, so it is scoped to schemas
|
|
21
|
-
* *response*. A request body is a different shape and legitimately carries
|
|
17
|
+
* Drops `hidden`, `secret`, and `visual` fields. `SignalContext.resolveReturn` strips the first two from every
|
|
18
|
+
* response, so naming them describes a value the caller can never read — and on a model like `user` the names
|
|
19
|
+
* are themselves the leak: `password`, `accountId`, `phone` published as readable properties of the model. This
|
|
20
|
+
* is the one place a field the framework blocks on every value path is still visible, so it is scoped to schemas
|
|
21
|
+
* that describe a *response*. A request body is a different shape and legitimately carries all three.
|
|
22
|
+
*
|
|
23
|
+
* `visual` is dropped here because it is dropped from the payload an AI caller receives, and a schema that
|
|
24
|
+
* promises a field the value omits is worse than one that never named it: a non-optional visual field would be
|
|
25
|
+
* listed `required` and a validating client would refuse the whole result.
|
|
22
26
|
*/
|
|
23
27
|
readable?: boolean;
|
|
24
28
|
}
|
|
@@ -56,7 +60,7 @@ export class JsonSchemaBuilder {
|
|
|
56
60
|
const required: string[] = [];
|
|
57
61
|
for (const [key, field] of Object.entries(fields)) {
|
|
58
62
|
const props = field.getProps();
|
|
59
|
-
if (readable && (props.fieldType === "hidden" || props.fieldType === "secret")) continue;
|
|
63
|
+
if (readable && (props.fieldType === "hidden" || props.fieldType === "secret" || props.visual)) continue;
|
|
60
64
|
properties[key] = this.#field(field);
|
|
61
65
|
if (!props.nullable) required.push(key);
|
|
62
66
|
}
|
|
@@ -51,6 +51,11 @@ const blockTags = new Set([
|
|
|
51
51
|
* agent's own UI is marked `data-agent-ui` and skipped, so a turn never re-reads its own transcript, and a
|
|
52
52
|
* password value is never read — the screen shows dots, so the DOM holds more than the user sees.
|
|
53
53
|
*
|
|
54
|
+
* A region the app marks `data-agent-skip` — what `Agent.Skip` renders — costs a `[skipped: <name>]` line instead
|
|
55
|
+
* of its text. It stands in the output rather than vanishing because a deleted region reads as an absent one, and
|
|
56
|
+
* an agent asked about a footer it never saw answers that the page has none. Naming the region as `section` reads
|
|
57
|
+
* it: the marker is what the default read leaves out, not a wall.
|
|
58
|
+
*
|
|
54
59
|
* A heading carries `(#anchor)` whenever it opens a container that has an id or a scope path, because that is the
|
|
55
60
|
* name `readScreen({ section })` and `highlight` take: printing the text without the name leaves an agent
|
|
56
61
|
* guessing at a slug. For the same reason a truncated read ends with the headings below the cut instead of only
|
|
@@ -64,7 +69,7 @@ export class ScreenReader {
|
|
|
64
69
|
const reader = new ScreenReader();
|
|
65
70
|
const title = document.title.trim();
|
|
66
71
|
if (title) reader.#lines.push(`Page: ${title}`);
|
|
67
|
-
reader.#walk(root ?? document.body);
|
|
72
|
+
reader.#walk(root ?? document.body, true);
|
|
68
73
|
reader.#flush();
|
|
69
74
|
return reader.#text() || "The page is rendering nothing readable.";
|
|
70
75
|
}
|
|
@@ -78,10 +83,10 @@ export class ScreenReader {
|
|
|
78
83
|
if (typeof document === "undefined") return "No rendered document is available.";
|
|
79
84
|
const reader = new ScreenReader();
|
|
80
85
|
const container = ScreenReader.#sectionOf(heading, root);
|
|
81
|
-
if (container) reader.#walk(container);
|
|
86
|
+
if (container) reader.#walk(container, true);
|
|
82
87
|
else {
|
|
83
88
|
const level = headingLevels[heading.tagName.toUpperCase() as keyof typeof headingLevels] ?? 1;
|
|
84
|
-
reader.#walk(heading);
|
|
89
|
+
reader.#walk(heading, true);
|
|
85
90
|
let next = heading.nextElementSibling;
|
|
86
91
|
while (next && !ScreenReader.#stops(next, level)) {
|
|
87
92
|
reader.#walk(next);
|
|
@@ -123,6 +128,17 @@ export class ScreenReader {
|
|
|
123
128
|
return "";
|
|
124
129
|
}
|
|
125
130
|
|
|
131
|
+
/**
|
|
132
|
+
* `checkVisibility()` reports whether the element has a layout box, and a `display: contents` wrapper has none
|
|
133
|
+
* while its children do — so Chrome answers false for a wrapper the user is looking straight at, and skipping it
|
|
134
|
+
* would drop that whole subtree (happy-dom answers true, so no DOM test sees the difference). Reading through it
|
|
135
|
+
* is safe only because the walk is top-down: a `display: none` ancestor bails on its own computed display first.
|
|
136
|
+
*/
|
|
137
|
+
static #rendered(el: HTMLElement) {
|
|
138
|
+
if (typeof el.checkVisibility !== "function" || el.checkVisibility()) return true;
|
|
139
|
+
return getComputedStyle(el).display === "contents";
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
static #stops(el: Element, level: number) {
|
|
127
143
|
const found = headingLevels[el.tagName.toUpperCase() as keyof typeof headingLevels];
|
|
128
144
|
return !!found && found <= level;
|
|
@@ -141,7 +157,7 @@ export class ScreenReader {
|
|
|
141
157
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
142
158
|
const el = node as HTMLElement;
|
|
143
159
|
const tag = el.tagName.toUpperCase();
|
|
144
|
-
if (skipTags.has(tag) || el.hasAttribute("data-agent-ui")) return;
|
|
160
|
+
if (skipTags.has(tag) || el.hasAttribute("data-agent-ui") || el.hasAttribute("data-agent-skip")) return;
|
|
145
161
|
const level = headingLevels[tag as keyof typeof headingLevels];
|
|
146
162
|
if (level) {
|
|
147
163
|
const anchor = ScreenReader.anchorOf(el);
|
|
@@ -163,7 +179,15 @@ export class ScreenReader {
|
|
|
163
179
|
: `${kept}${note}`;
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
|
|
182
|
+
/** What stands where a skipped region was: its own name, and the anchor `section` takes to read it on request. */
|
|
183
|
+
#mark(el: HTMLElement, label: string) {
|
|
184
|
+
const anchor = el.getAttribute("data-agent-scope") ?? el.getAttribute("data-agent-zone") ?? el.id;
|
|
185
|
+
this.#flush();
|
|
186
|
+
this.#buffer = `[skipped: ${label || el.tagName.toLowerCase()}${anchor ? ` (#${anchor})` : ""}]`;
|
|
187
|
+
this.#flush();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#walk(node: Node, isRoot = false): void {
|
|
167
191
|
if (this.#length > ScreenReader.limit * 2) {
|
|
168
192
|
this.#outline(node);
|
|
169
193
|
return;
|
|
@@ -179,7 +203,13 @@ export class ScreenReader {
|
|
|
179
203
|
if (skipTags.has(tag)) return;
|
|
180
204
|
if (el.hasAttribute("data-agent-ui") || el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true")
|
|
181
205
|
return;
|
|
182
|
-
if (
|
|
206
|
+
if (!ScreenReader.#rendered(el)) return;
|
|
207
|
+
|
|
208
|
+
const skipped = isRoot ? null : el.getAttribute("data-agent-skip");
|
|
209
|
+
if (skipped !== null) {
|
|
210
|
+
this.#mark(el, skipped);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
183
213
|
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
|
|
184
214
|
this.#control(el, tag);
|
|
185
215
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ScreenReader } from "./ScreenReader";
|
|
2
2
|
|
|
3
|
-
const containerAttrs = ["data-agent-zone", "data-agent-scope"] as const;
|
|
3
|
+
const containerAttrs = ["data-agent-zone", "data-agent-scope", "data-agent-skip"] as const;
|
|
4
4
|
const controlAttrs = ["data-akan-action", "data-akan-state"] as const;
|
|
5
5
|
const headingSelector = "h1, h2, h3, h4, h5, h6";
|
|
6
6
|
const nameCap = 40;
|
|
@@ -9,9 +9,9 @@ const nameCap = 40;
|
|
|
9
9
|
* Resolves a name the agent read on screen to the element it names.
|
|
10
10
|
*
|
|
11
11
|
* Four vocabularies, every one of them something already on the screen rather than a selector the model invented:
|
|
12
|
-
* the `data-akan-action` / `data-akan-state` annotation a control carries and `readScreen` prints beside it,
|
|
13
|
-
* `Agent.Zone
|
|
14
|
-
* **heading by its own text**.
|
|
12
|
+
* the `data-akan-action` / `data-akan-state` annotation a control carries and `readScreen` prints beside it, a
|
|
13
|
+
* container — an `Agent.Zone`, a `useScreenScope` scope, or an `Agent.Skip` region named by the marker left in its
|
|
14
|
+
* place — a plain element id, what a docs slide is addressed by, and, last, a **heading by its own text**.
|
|
15
15
|
*
|
|
16
16
|
* A heading is matched on letters and digits alone, so the slug an agent naturally writes for a heading it read
|
|
17
17
|
* ("images-and-public-env") finds "Images And Public Env". That tolerance is for headings only: a heading is a
|
|
@@ -20,7 +20,8 @@ const nameCap = 40;
|
|
|
20
20
|
*
|
|
21
21
|
* Nothing hidden ever resolves, by the same rule `readScreen` skips it. A collapsed panel or an off-variant
|
|
22
22
|
* duplicate is not what the user is looking at, and scrolling to one flashes a ring nobody can see — which reads
|
|
23
|
-
* as the tool being broken rather than as a miss.
|
|
23
|
+
* as the tool being broken rather than as a miss. A `display: contents` wrapper does not resolve either, even
|
|
24
|
+
* though the reader now reads through it: it has no box, so `scrollIntoView` has nothing to scroll to.
|
|
24
25
|
*/
|
|
25
26
|
export class ScreenTarget {
|
|
26
27
|
static container(name: string, root?: HTMLElement | null): HTMLElement | null {
|
|
@@ -28,8 +29,13 @@ export class ScreenTarget {
|
|
|
28
29
|
if (!scope || !name) return null;
|
|
29
30
|
if (ScreenTarget.#named(scope, containerAttrs, name)) return scope;
|
|
30
31
|
const escaped = CSS.escape(name);
|
|
31
|
-
const selector =
|
|
32
|
-
|
|
32
|
+
const selector = containerAttrs.map((attr) => `[${attr}="${escaped}"]`).join(", ");
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
ScreenTarget.#first(scope, selector) ??
|
|
36
|
+
[...scope.querySelectorAll<HTMLElement>("[id]")].find((el) => el.id === name && ScreenTarget.#visible(el)) ??
|
|
37
|
+
null
|
|
38
|
+
);
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
static control(name: string, root?: HTMLElement | null): HTMLElement | null {
|
|
@@ -107,7 +107,7 @@ export class StoreSurfaceSource implements SurfaceSource {
|
|
|
107
107
|
section: {
|
|
108
108
|
type: "string",
|
|
109
109
|
description:
|
|
110
|
-
"One region of the screen: a heading's anchor as readScreen prints it, the heading's own text,
|
|
110
|
+
"One region of the screen: a heading's anchor as readScreen prints it, the heading's own text, a scope path from the screen context, or the name in a `[skipped: name]` marker. Omit to read all of it.",
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
113
|
additionalProperties: false,
|
|
@@ -47,6 +47,13 @@ export interface ConstantFieldProps<FieldType extends ConstantFieldKind = Consta
|
|
|
47
47
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
48
48
|
text?: TextFieldRole;
|
|
49
49
|
cascade?: CascadeAction;
|
|
50
|
+
/**
|
|
51
|
+
* Renders on the page, never reaches an agent. Stripped wherever a value is masked for an AI caller — the
|
|
52
|
+
* in-page agent's reads and every MCP result — and left untouched everywhere else, so a `File`'s blur
|
|
53
|
+
* placeholder still ships to `<Image>`. Unlike `hidden`/`secret` this is about cost, not secrecy: a field
|
|
54
|
+
* nothing can answer a question with is pure spend on every turn it rides.
|
|
55
|
+
*/
|
|
56
|
+
visual?: boolean;
|
|
50
57
|
meta?: Metadata;
|
|
51
58
|
}
|
|
52
59
|
export declare const fieldPresets: readonly ["email", "password", "url"];
|
|
@@ -94,6 +101,7 @@ interface ConstantFieldBuildProps<FieldType extends ConstantFieldKind = any, Fie
|
|
|
94
101
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
95
102
|
text?: TextFieldRole;
|
|
96
103
|
cascade?: CascadeAction;
|
|
104
|
+
visual: boolean;
|
|
97
105
|
modelRef: ConstantModelRef;
|
|
98
106
|
arrDepth: number;
|
|
99
107
|
optArrDepth: number;
|
|
@@ -139,6 +147,7 @@ export declare class ConstantField<FieldType extends ConstantFieldKind = Constan
|
|
|
139
147
|
readonly validate?: (value: FieldValue, model: any) => boolean;
|
|
140
148
|
readonly text?: TextFieldRole;
|
|
141
149
|
readonly cascade?: CascadeAction;
|
|
150
|
+
readonly visual: boolean;
|
|
142
151
|
readonly modelRef: ConstantModelRef;
|
|
143
152
|
readonly arrDepth: number;
|
|
144
153
|
readonly optArrDepth: number;
|
|
@@ -172,6 +181,7 @@ export type PlainTypeToFieldType<PlainType> = PlainType extends [infer First, ..
|
|
|
172
181
|
/** Builds a stored property field with optional validation, default, ref, text, and metadata options. */
|
|
173
182
|
export declare const field: {
|
|
174
183
|
<ExplicitType, Value extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never>(value: Value, option?: FieldOption<Value, MapValue>): FieldInfo<"property", Value, ExplicitType, MapValue, FieldKindSource<Value>, UnCls<FieldKindSource<Value>> extends BaseObject ? true : false, FieldKindSource<Value> extends infer T ? T extends FieldKindSource<Value> ? T extends EnumInstance<string, any> ? true : false : never : never, FieldKindSource<Value> extends infer T_1 ? T_1 extends FieldKindSource<Value> ? T_1 extends typeof PrimitiveScalar ? true : false : never : never, FieldKindSource<Value> extends infer T_2 ? T_2 extends FieldKindSource<Value> ? T_2 extends MapConstructor ? true : false : never : never, (UnCls<FieldKindSource<Value>> extends BaseObject ? true : false) extends infer T_3 ? T_3 extends (UnCls<FieldKindSource<Value>> extends BaseObject ? true : false) ? T_3 extends true ? false : (FieldKindSource<Value> extends infer T_4 ? T_4 extends FieldKindSource<Value> ? T_4 extends EnumInstance<string, any> ? true : false : never : never) extends infer T_5 ? T_5 extends (FieldKindSource<Value> extends infer T_12 ? T_12 extends FieldKindSource<Value> ? T_12 extends EnumInstance<string, any> ? true : false : never : never) ? T_5 extends true ? false : (FieldKindSource<Value> extends infer T_6 ? T_6 extends FieldKindSource<Value> ? T_6 extends typeof PrimitiveScalar ? true : false : never : never) extends infer T_7 ? T_7 extends (FieldKindSource<Value> extends infer T_11 ? T_11 extends FieldKindSource<Value> ? T_11 extends typeof PrimitiveScalar ? true : false : never : never) ? T_7 extends true ? false : (FieldKindSource<Value> extends infer T_8 ? T_8 extends FieldKindSource<Value> ? T_8 extends MapConstructor ? true : false : never : never) extends infer T_9 ? T_9 extends (FieldKindSource<Value> extends infer T_10 ? T_10 extends FieldKindSource<Value> ? T_10 extends MapConstructor ? true : false : never : never) ? T_9 extends true ? false : true : never : never : never : never : never : never : never : never, false, false>;
|
|
184
|
+
visual<ExplicitType, Value extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never>(value: Value, option?: FieldOption<Value, MapValue>): FieldInfo<"property", Value, ExplicitType, MapValue, FieldKindSource<Value>, UnCls<FieldKindSource<Value>> extends BaseObject ? true : false, FieldKindSource<Value> extends infer T ? T extends FieldKindSource<Value> ? T extends EnumInstance<string, any> ? true : false : never : never, FieldKindSource<Value> extends infer T_1 ? T_1 extends FieldKindSource<Value> ? T_1 extends typeof PrimitiveScalar ? true : false : never : never, FieldKindSource<Value> extends infer T_2 ? T_2 extends FieldKindSource<Value> ? T_2 extends MapConstructor ? true : false : never : never, (UnCls<FieldKindSource<Value>> extends BaseObject ? true : false) extends infer T_3 ? T_3 extends (UnCls<FieldKindSource<Value>> extends BaseObject ? true : false) ? T_3 extends true ? false : (FieldKindSource<Value> extends infer T_4 ? T_4 extends FieldKindSource<Value> ? T_4 extends EnumInstance<string, any> ? true : false : never : never) extends infer T_5 ? T_5 extends (FieldKindSource<Value> extends infer T_12 ? T_12 extends FieldKindSource<Value> ? T_12 extends EnumInstance<string, any> ? true : false : never : never) ? T_5 extends true ? false : (FieldKindSource<Value> extends infer T_6 ? T_6 extends FieldKindSource<Value> ? T_6 extends typeof PrimitiveScalar ? true : false : never : never) extends infer T_7 ? T_7 extends (FieldKindSource<Value> extends infer T_11 ? T_11 extends FieldKindSource<Value> ? T_11 extends typeof PrimitiveScalar ? true : false : never : never) ? T_7 extends true ? false : (FieldKindSource<Value> extends infer T_8 ? T_8 extends FieldKindSource<Value> ? T_8 extends MapConstructor ? true : false : never : never) extends infer T_9 ? T_9 extends (FieldKindSource<Value> extends infer T_10 ? T_10 extends FieldKindSource<Value> ? T_10 extends MapConstructor ? true : false : never : never) ? T_9 extends true ? false : true : never : never : never : never : never : never : never : never, false, false>;
|
|
175
185
|
hidden<ExplicitType, Value extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never>(value: Value, option?: FieldOption<Value, MapValue>): FieldInfo<"hidden", Value, ExplicitType, MapValue, FieldKindSource<Value>, UnCls<FieldKindSource<Value>> extends BaseObject ? true : false, FieldKindSource<Value> extends infer T ? T extends FieldKindSource<Value> ? T extends EnumInstance<string, any> ? true : false : never : never, FieldKindSource<Value> extends infer T_1 ? T_1 extends FieldKindSource<Value> ? T_1 extends typeof PrimitiveScalar ? true : false : never : never, FieldKindSource<Value> extends infer T_2 ? T_2 extends FieldKindSource<Value> ? T_2 extends MapConstructor ? true : false : never : never, (UnCls<FieldKindSource<Value>> extends BaseObject ? true : false) extends infer T_3 ? T_3 extends (UnCls<FieldKindSource<Value>> extends BaseObject ? true : false) ? T_3 extends true ? false : (FieldKindSource<Value> extends infer T_4 ? T_4 extends FieldKindSource<Value> ? T_4 extends EnumInstance<string, any> ? true : false : never : never) extends infer T_5 ? T_5 extends (FieldKindSource<Value> extends infer T_12 ? T_12 extends FieldKindSource<Value> ? T_12 extends EnumInstance<string, any> ? true : false : never : never) ? T_5 extends true ? false : (FieldKindSource<Value> extends infer T_6 ? T_6 extends FieldKindSource<Value> ? T_6 extends typeof PrimitiveScalar ? true : false : never : never) extends infer T_7 ? T_7 extends (FieldKindSource<Value> extends infer T_11 ? T_11 extends FieldKindSource<Value> ? T_11 extends typeof PrimitiveScalar ? true : false : never : never) ? T_7 extends true ? false : (FieldKindSource<Value> extends infer T_8 ? T_8 extends FieldKindSource<Value> ? T_8 extends MapConstructor ? true : false : never : never) extends infer T_9 ? T_9 extends (FieldKindSource<Value> extends infer T_10 ? T_10 extends FieldKindSource<Value> ? T_10 extends MapConstructor ? true : false : never : never) ? T_9 extends true ? false : true : never : never : never : never : never : never : never : never, true, false>;
|
|
176
186
|
secret<ExplicitType, Value extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never>(value: Value, option?: FieldOption<Value, MapValue>): FieldInfo<"secret", Value | null, ExplicitType | null, MapValue, SingleValue<Value> & {}, UnCls<SingleValue<Value> & {}> extends BaseObject ? true : false, SingleValue<Value> & {} extends infer T ? T extends SingleValue<Value> & {} ? T extends EnumInstance<string, any> ? true : false : never : never, SingleValue<Value> & {} extends infer T_1 ? T_1 extends SingleValue<Value> & {} ? T_1 extends typeof PrimitiveScalar ? true : false : never : never, SingleValue<Value> & {} extends infer T_2 ? T_2 extends SingleValue<Value> & {} ? T_2 extends MapConstructor ? true : false : never : never, (UnCls<SingleValue<Value> & {}> extends BaseObject ? true : false) extends infer T_3 ? T_3 extends (UnCls<SingleValue<Value> & {}> extends BaseObject ? true : false) ? T_3 extends true ? false : (SingleValue<Value> & {} extends infer T_4 ? T_4 extends SingleValue<Value> & {} ? T_4 extends EnumInstance<string, any> ? true : false : never : never) extends infer T_5 ? T_5 extends (SingleValue<Value> & {} extends infer T_12 ? T_12 extends SingleValue<Value> & {} ? T_12 extends EnumInstance<string, any> ? true : false : never : never) ? T_5 extends true ? false : (SingleValue<Value> & {} extends infer T_6 ? T_6 extends SingleValue<Value> & {} ? T_6 extends typeof PrimitiveScalar ? true : false : never : never) extends infer T_7 ? T_7 extends (SingleValue<Value> & {} extends infer T_11 ? T_11 extends SingleValue<Value> & {} ? T_11 extends typeof PrimitiveScalar ? true : false : never : never) ? T_7 extends true ? false : (SingleValue<Value> & {} extends infer T_8 ? T_8 extends SingleValue<Value> & {} ? T_8 extends MapConstructor ? true : false : never : never) extends infer T_9 ? T_9 extends (SingleValue<Value> & {} extends infer T_10 ? T_10 extends SingleValue<Value> & {} ? T_10 extends MapConstructor ? true : false : never : never) ? T_9 extends true ? false : true : never : never : never : never : never : never : never : never, false, true>;
|
|
177
187
|
};
|
package/types/constant/mask.d.ts
CHANGED
|
@@ -12,13 +12,20 @@ interface MaskField {
|
|
|
12
12
|
fieldType?: string;
|
|
13
13
|
isClass?: boolean;
|
|
14
14
|
modelRef?: MaskModel;
|
|
15
|
+
visual?: boolean;
|
|
15
16
|
}
|
|
16
17
|
export declare const maskFieldsOf: (model: MaskModel) => Record<string, MaskField> | null;
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The `hidden` and `secret` field names of `model` that `value` still carries populated.
|
|
20
|
+
*
|
|
21
|
+
* `visual` is deliberately not among them. A refusal here means a value must not be published at all, and a blur
|
|
22
|
+
* placeholder is not a secret — it is merely not worth its tokens, which masking answers by dropping it.
|
|
23
|
+
*/
|
|
18
24
|
export declare const leakingFieldsOf: (model: MaskModel, value: Record<string, unknown>) => string[];
|
|
19
25
|
/**
|
|
20
|
-
* Strips what a model marks `hidden` or `
|
|
21
|
-
* happens to still carry.
|
|
26
|
+
* Strips what a model marks `hidden`, `secret`, or `visual`, by the model the caller names rather than by the one
|
|
27
|
+
* the value happens to still carry. The first two are secrecy and the third is cost, but the answer is the same
|
|
28
|
+
* one — leave the field out — and this is the only place every AI-facing read already passes through.
|
|
22
29
|
*
|
|
23
30
|
* That distinction is the whole point. A check that reads the class off the value can only mask what arrives as an
|
|
24
31
|
* instance, so a `{ ...doc }` spread, a `toJSON()`, an `immerify()`, or a round-trip through `JSON.stringify` reaches
|
|
@@ -52,6 +52,13 @@ export interface McpServerOption {
|
|
|
52
52
|
* the document is built once at boot and cached by clients, and it is read by a model rather than by a person.
|
|
53
53
|
*/
|
|
54
54
|
language?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Whether a structured result also ships as serialized JSON in the text block, `true` by default because that
|
|
57
|
+
* is what the spec asks of a server for clients that predate `structuredContent`. It is also a flat doubling:
|
|
58
|
+
* every model-returning tool sends its whole payload twice, so a deployment whose clients read the structured
|
|
59
|
+
* half turns this off and halves what each of those calls costs the model. `AKAN_MCP_LEGACY_TEXT=false`.
|
|
60
|
+
*/
|
|
61
|
+
legacyTextBlock?: boolean;
|
|
55
62
|
/** OAuth resource-server identity: which issuers a client may authenticate with, and the scopes to demand. */
|
|
56
63
|
auth?: McpAuthOption;
|
|
57
64
|
}
|
|
@@ -24,11 +24,18 @@ interface McpDispatcherProps {
|
|
|
24
24
|
middleware: Map<string, MiddlewareCls>;
|
|
25
25
|
/** The one language error text is resolved in, matching the catalogue the client was handed. */
|
|
26
26
|
language?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a structured result also ships as serialized JSON in the text block. `true` by default, which is what
|
|
29
|
+
* the spec asks for; `false` halves what every model-returning tool costs.
|
|
30
|
+
*/
|
|
31
|
+
legacyTextBlock?: boolean;
|
|
27
32
|
}
|
|
28
33
|
/** Executes one MCP tool call or resource read through the ordinary signal pipeline. */
|
|
29
34
|
export declare class McpDispatcher {
|
|
30
35
|
#private;
|
|
31
36
|
static readonly logger: Logger;
|
|
37
|
+
/** What the text block says when the serialized duplicate is off. Read by a model, so English. */
|
|
38
|
+
static readonly structuredNote = "The result is in this call's structuredContent.";
|
|
32
39
|
constructor(props: McpDispatcherProps);
|
|
33
40
|
call(exposed: McpExposedEndpoint, args: Record<string, unknown>, req: Request): Promise<McpToolResult>;
|
|
34
41
|
/**
|
|
@@ -26,6 +26,8 @@ export interface McpRouterProps {
|
|
|
26
26
|
* rarely sees. Defaults to `en`, falling back to the first registered language when the app has no `en`.
|
|
27
27
|
*/
|
|
28
28
|
language?: string;
|
|
29
|
+
/** Whether a structured result also ships as serialized JSON in the text block. Default `true`. */
|
|
30
|
+
legacyTextBlock?: boolean;
|
|
29
31
|
auth?: McpAuthOption;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
@@ -10,11 +10,15 @@ export interface JsonSchemaBuilderOptions {
|
|
|
10
10
|
}
|
|
11
11
|
export interface JsonSchemaModelOptions {
|
|
12
12
|
/**
|
|
13
|
-
* Drops `hidden` and `
|
|
14
|
-
* them describes a value the caller can never read — and on a model like `user` the names
|
|
15
|
-
* leak: `password`, `accountId`, `phone` published as readable properties of the model. This
|
|
16
|
-
* field the framework blocks on every value path is still visible, so it is scoped to schemas
|
|
17
|
-
* *response*. A request body is a different shape and legitimately carries
|
|
13
|
+
* Drops `hidden`, `secret`, and `visual` fields. `SignalContext.resolveReturn` strips the first two from every
|
|
14
|
+
* response, so naming them describes a value the caller can never read — and on a model like `user` the names
|
|
15
|
+
* are themselves the leak: `password`, `accountId`, `phone` published as readable properties of the model. This
|
|
16
|
+
* is the one place a field the framework blocks on every value path is still visible, so it is scoped to schemas
|
|
17
|
+
* that describe a *response*. A request body is a different shape and legitimately carries all three.
|
|
18
|
+
*
|
|
19
|
+
* `visual` is dropped here because it is dropped from the payload an AI caller receives, and a schema that
|
|
20
|
+
* promises a field the value omits is worse than one that never named it: a non-optional visual field would be
|
|
21
|
+
* listed `required` and a validating client would refuse the whole result.
|
|
18
22
|
*/
|
|
19
23
|
readable?: boolean;
|
|
20
24
|
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* agent's own UI is marked `data-agent-ui` and skipped, so a turn never re-reads its own transcript, and a
|
|
5
5
|
* password value is never read — the screen shows dots, so the DOM holds more than the user sees.
|
|
6
6
|
*
|
|
7
|
+
* A region the app marks `data-agent-skip` — what `Agent.Skip` renders — costs a `[skipped: <name>]` line instead
|
|
8
|
+
* of its text. It stands in the output rather than vanishing because a deleted region reads as an absent one, and
|
|
9
|
+
* an agent asked about a footer it never saw answers that the page has none. Naming the region as `section` reads
|
|
10
|
+
* it: the marker is what the default read leaves out, not a wall.
|
|
11
|
+
*
|
|
7
12
|
* A heading carries `(#anchor)` whenever it opens a container that has an id or a scope path, because that is the
|
|
8
13
|
* name `readScreen({ section })` and `highlight` take: printing the text without the name leaves an agent
|
|
9
14
|
* guessing at a slug. For the same reason a truncated read ends with the headings below the cut instead of only
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Resolves a name the agent read on screen to the element it names.
|
|
3
3
|
*
|
|
4
4
|
* Four vocabularies, every one of them something already on the screen rather than a selector the model invented:
|
|
5
|
-
* the `data-akan-action` / `data-akan-state` annotation a control carries and `readScreen` prints beside it,
|
|
6
|
-
* `Agent.Zone
|
|
7
|
-
* **heading by its own text**.
|
|
5
|
+
* the `data-akan-action` / `data-akan-state` annotation a control carries and `readScreen` prints beside it, a
|
|
6
|
+
* container — an `Agent.Zone`, a `useScreenScope` scope, or an `Agent.Skip` region named by the marker left in its
|
|
7
|
+
* place — a plain element id, what a docs slide is addressed by, and, last, a **heading by its own text**.
|
|
8
8
|
*
|
|
9
9
|
* A heading is matched on letters and digits alone, so the slug an agent naturally writes for a heading it read
|
|
10
10
|
* ("images-and-public-env") finds "Images And Public Env". That tolerance is for headings only: a heading is a
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Nothing hidden ever resolves, by the same rule `readScreen` skips it. A collapsed panel or an off-variant
|
|
15
15
|
* duplicate is not what the user is looking at, and scrolling to one flashes a ring nobody can see — which reads
|
|
16
|
-
* as the tool being broken rather than as a miss.
|
|
16
|
+
* as the tool being broken rather than as a miss. A `display: contents` wrapper does not resolve either, even
|
|
17
|
+
* though the reader now reads through it: it has no box, so `scrollIntoView` has nothing to scroll to.
|
|
17
18
|
*/
|
|
18
19
|
export declare class ScreenTarget {
|
|
19
20
|
#private;
|