@google-cloud/kcmd 0.0.0-stage → 0.2.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/LICENSE +203 -0
- package/README.md +264 -2
- package/build/ts/kcmd/cli.js +20252 -0
- package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
- package/build/ts/kcmd/gcp/alloydb.js +70 -0
- package/build/ts/kcmd/gcp/api.d.ts +29 -0
- package/build/ts/kcmd/gcp/api.js +162 -0
- package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
- package/build/ts/kcmd/gcp/biglake.js +59 -0
- package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
- package/build/ts/kcmd/gcp/bigquery.js +134 -0
- package/build/ts/kcmd/gcp/context.d.ts +10 -0
- package/build/ts/kcmd/gcp/context.js +74 -0
- package/build/ts/kcmd/gcp/crm.d.ts +13 -0
- package/build/ts/kcmd/gcp/crm.js +91 -0
- package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
- package/build/ts/kcmd/gcp/dataplex.js +406 -0
- package/build/ts/kcmd/gcp/index.d.ts +3 -0
- package/build/ts/kcmd/gcp/index.js +21 -0
- package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
- package/build/ts/kcmd/gcp/spanner.js +126 -0
- package/build/ts/kcmd/index.d.ts +7 -0
- package/build/ts/kcmd/index.js +46 -0
- package/build/ts/kcmd/layout.d.ts +16 -0
- package/build/ts/kcmd/layout.js +31 -0
- package/build/ts/kcmd/layouts/documents.d.ts +24 -0
- package/build/ts/kcmd/layouts/documents.js +264 -0
- package/build/ts/kcmd/layouts/okf.d.ts +31 -0
- package/build/ts/kcmd/layouts/okf.js +386 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
- package/build/ts/kcmd/layouts/semantic-model.js +621 -0
- package/build/ts/kcmd/layouts/standard.d.ts +13 -0
- package/build/ts/kcmd/layouts/standard.js +108 -0
- package/build/ts/kcmd/manifest.d.ts +34 -0
- package/build/ts/kcmd/manifest.js +262 -0
- package/build/ts/kcmd/metadata.d.ts +25 -0
- package/build/ts/kcmd/metadata.js +8 -0
- package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
- package/build/ts/kcmd/okf/catalog_residue.js +124 -0
- package/build/ts/kcmd/okf/concept.d.ts +14 -0
- package/build/ts/kcmd/okf/concept.js +110 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
- package/build/ts/kcmd/okf/links.d.ts +23 -0
- package/build/ts/kcmd/okf/links.js +468 -0
- package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
- package/build/ts/kcmd/okf/okf_metadata.js +99 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +469 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +92 -0
- package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
- package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
- package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
- package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
- package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
- package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
- package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
- package/build/ts/kcmd/semantic/deployment_target.js +379 -0
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
- package/build/ts/kcmd/semantic/import_model.js +1698 -0
- package/build/ts/kcmd/semantic/ir.d.ts +589 -0
- package/build/ts/kcmd/semantic/ir.js +247 -0
- package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
- package/build/ts/kcmd/semantic/kc_actions.js +531 -0
- package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
- package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
- package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
- package/build/ts/kcmd/semantic/kc_converter.js +839 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
- package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
- package/build/ts/kcmd/semantic/kc_entries.js +284 -0
- package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
- package/build/ts/kcmd/semantic/kc_ids.js +188 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
- package/build/ts/kcmd/semantic/loader.d.ts +258 -0
- package/build/ts/kcmd/semantic/loader.js +3576 -0
- package/build/ts/kcmd/semantic/names.d.ts +10 -0
- package/build/ts/kcmd/semantic/names.js +85 -0
- package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
- package/build/ts/kcmd/semantic/osi_converter.js +910 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
- package/build/ts/kcmd/semantic/parameters.js +171 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
- package/build/ts/kcmd/semantic/pull_kc.js +272 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
- package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
- package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
- package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
- package/build/ts/kcmd/semantic/runtime/store.js +344 -0
- package/build/ts/kcmd/semantic/skills.d.ts +110 -0
- package/build/ts/kcmd/semantic/skills.js +697 -0
- package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
- package/build/ts/kcmd/semantic/source_dialect.js +311 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +341 -0
- package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
- package/build/ts/kcmd/semantic/spanner.js +60 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
- package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
- package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
- package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
- package/build/ts/kcmd/semantic/sql_parser.js +400 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
- package/build/ts/kcmd/semantic/transpile.js +581 -0
- package/build/ts/kcmd/semantic/validate.d.ts +70 -0
- package/build/ts/kcmd/semantic/validate.js +2266 -0
- package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
- package/build/ts/kcmd/semantic/yaml_options.js +58 -0
- package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
- package/build/ts/kcmd/semantic/yaml_update.js +782 -0
- package/build/ts/kcmd/snapshot.d.ts +37 -0
- package/build/ts/kcmd/snapshot.js +386 -0
- package/build/ts/kcmd/source.d.ts +31 -0
- package/build/ts/kcmd/source.js +119 -0
- package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
- package/build/ts/kcmd/sources/bq-dataset.js +131 -0
- package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
- package/build/ts/kcmd/sources/entrygroup.js +83 -0
- package/build/ts/kcmd/sources/kb.d.ts +21 -0
- package/build/ts/kcmd/sources/kb.js +82 -0
- package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
- package/build/ts/kcmd/sources/semantic-model.js +48 -0
- package/build/ts/kcmd/sync.d.ts +35 -0
- package/build/ts/kcmd/sync.js +734 -0
- package/package.json +62 -4
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Generating an Agent Skill from a semantic model.
|
|
4
|
+
*
|
|
5
|
+
* An Agent Skill (agentskills.io) is a directory holding a `SKILL.md` -- YAML
|
|
6
|
+
* frontmatter naming the skill, then Markdown telling an agent how to do
|
|
7
|
+
* something. Clients load the frontmatter at startup and the body only when
|
|
8
|
+
* the skill looks relevant, so the body has a budget: under 500 lines, and
|
|
9
|
+
* under about 5,000 tokens. Anything longer goes in files the body points at,
|
|
10
|
+
* which the agent reads only if it needs them.
|
|
11
|
+
*
|
|
12
|
+
* That budget decides the shape of what this emits. A model's actions carry
|
|
13
|
+
* their parameters, the rules that gate them and the blast radius of each
|
|
14
|
+
* call, and inlining all of it puts a page of detail about every action in
|
|
15
|
+
* front of an agent that wanted one of them. So `SKILL.md` is a ROUTER: what
|
|
16
|
+
* the model is, a one-line table of the actions, and the parts that are true
|
|
17
|
+
* of every call. Each action's detail is a file under `references/`, named in
|
|
18
|
+
* the table, read on demand. A model with thirty actions costs the same at
|
|
19
|
+
* startup as a model with one.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here decides what an action means. `modelTools` already turns a
|
|
22
|
+
* bound model into the action descriptors a skill needs -- the tool name, the
|
|
23
|
+
* description with the gating rules folded in, the typed parameters, and
|
|
24
|
+
* whether this binding can run it -- so this module renders what `modelTools`
|
|
25
|
+
* returns and derives nothing of its own beyond layout.
|
|
26
|
+
*
|
|
27
|
+
* The skill is written against the LOGICAL model, which is what makes it worth
|
|
28
|
+
* generating once. An action's name, arguments, rules and blast radius are the
|
|
29
|
+
* same wherever it is deployed, because an executor is a physical binding that
|
|
30
|
+
* a profile supplies. So every `references/` page is a fact about the model
|
|
31
|
+
* and nothing else, and everything that reads the binding lives in `SKILL.md`:
|
|
32
|
+
* the store, the executor kinds, which actions this deployment cannot run and
|
|
33
|
+
* why, and the command line to try one with, all gathered under "Running an
|
|
34
|
+
* action", plus the snippet under "Finding a record" that reads the store
|
|
35
|
+
* directly, which needs the store's kind and its coordinates. Point this at a
|
|
36
|
+
* different profile and those move and the reference pages do not.
|
|
37
|
+
*
|
|
38
|
+
* Keeping that true takes some discipline: whether an action is RUNNABLE is a
|
|
39
|
+
* binding fact wearing a logical name, and putting `tool.unavailable` on the
|
|
40
|
+
* action's own page -- which reads naturally, and which this module did at
|
|
41
|
+
* first -- quietly makes every page profile-specific.
|
|
42
|
+
*/
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.generateSkill = generateSkill;
|
|
45
|
+
exports.skillNameFor = skillNameFor;
|
|
46
|
+
exports.whyNameIsInvalid = whyNameIsInvalid;
|
|
47
|
+
const agent_tools_1 = require("./runtime/agent_tools");
|
|
48
|
+
const dialect_1 = require("./runtime/dialect");
|
|
49
|
+
const runtime_1 = require("./runtime/runtime");
|
|
50
|
+
const store_1 = require("./runtime/store");
|
|
51
|
+
/**
|
|
52
|
+
* The skill for one model, as files a caller writes out.
|
|
53
|
+
*
|
|
54
|
+
* Returns an error for a name the Agent Skills spec would reject. It also
|
|
55
|
+
* returns the store error when the model's deployments are rejected, and
|
|
56
|
+
* when the profile names a store that cannot be resolved for a model with
|
|
57
|
+
* actions, whose skill names the store. A model with no actions is not an
|
|
58
|
+
* error -- it is a skill that says the model declares nothing to act on, which
|
|
59
|
+
* is the honest thing to hand an agent -- but it is warned about, because it
|
|
60
|
+
* is rarely what the caller meant.
|
|
61
|
+
*/
|
|
62
|
+
function generateSkill(opts) {
|
|
63
|
+
const { runtime } = opts;
|
|
64
|
+
const model = runtime.model;
|
|
65
|
+
const name = opts.name ?? skillNameFor(model.name);
|
|
66
|
+
const nameError = whyNameIsInvalid(name);
|
|
67
|
+
if (nameError)
|
|
68
|
+
return { error: nameError };
|
|
69
|
+
const { actions, instruction } = (0, agent_tools_1.modelTools)({ runtime });
|
|
70
|
+
// The skill names the store only in what it says about actions. So a store
|
|
71
|
+
// the profile names and that cannot be resolved stops only a skill with
|
|
72
|
+
// actions. Deployments the runtime marks rejected stop any skill.
|
|
73
|
+
if (runtime.deploymentsRejected || (runtime.storeFailed && actions.length)) {
|
|
74
|
+
return { error: (0, runtime_1.runtimeStoreError)(runtime) ?? '' };
|
|
75
|
+
}
|
|
76
|
+
const warnings = [];
|
|
77
|
+
if (!actions.length) {
|
|
78
|
+
warnings.push(`Model '${model.name}' declares no actions, so the skill describes ` +
|
|
79
|
+
`nothing an agent can do. Generated anyway.`);
|
|
80
|
+
}
|
|
81
|
+
else if (!actions.some(t => t.runnable)) {
|
|
82
|
+
// A skill whose every action is blocked still loads, still costs context
|
|
83
|
+
// on every request, and still advertises the model as the write path in
|
|
84
|
+
// frontmatter a client reads before the body. Saying so is the difference
|
|
85
|
+
// between an artifact the caller chose and one they did not notice.
|
|
86
|
+
warnings.push(`No action in '${model.name}' is runnable under profile '${runtime.profile}', so the skill describes ${actions.length} action${actions.length === 1 ? '' : 's'} and can ` +
|
|
87
|
+
`run none of them. "Running an action" in the skill gives the ` +
|
|
88
|
+
`reason for each.`);
|
|
89
|
+
}
|
|
90
|
+
const paths = referencePaths(actions);
|
|
91
|
+
const files = [{
|
|
92
|
+
path: 'SKILL.md',
|
|
93
|
+
text: skillDocument(name, runtime, actions, instruction, paths),
|
|
94
|
+
}];
|
|
95
|
+
for (const tool of actions) {
|
|
96
|
+
const action = actionFor(tool, model);
|
|
97
|
+
if (!action)
|
|
98
|
+
continue;
|
|
99
|
+
files.push({
|
|
100
|
+
path: paths.get(tool.actionName),
|
|
101
|
+
text: referenceDocument(tool, action, model),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
warnings.push(...overBudget(files[0].text));
|
|
105
|
+
return { name, files, warnings };
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// The name, which is also the directory name.
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/**
|
|
111
|
+
* A model name in the form the spec requires: 1-64 characters of lowercase
|
|
112
|
+
* `a-z0-9-`, with no leading, trailing or doubled hyphen.
|
|
113
|
+
*
|
|
114
|
+
* `Commerce` and `commerce_demo` are both ordinary model names and neither is
|
|
115
|
+
* a legal skill name, so this is a conversion rather than a check. What it
|
|
116
|
+
* cannot fix -- a name with no alphanumeric character in it at all -- falls
|
|
117
|
+
* through to the validator, which says so.
|
|
118
|
+
*/
|
|
119
|
+
function skillNameFor(modelName) {
|
|
120
|
+
return modelName.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
123
|
+
.replace(/^-+|-+$/g, '')
|
|
124
|
+
.slice(0, 64)
|
|
125
|
+
.replace(/-+$/, '');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Why this name is not a legal skill name, or undefined if it is.
|
|
129
|
+
*
|
|
130
|
+
* Worth being strict about here rather than leaving to the client. A skill
|
|
131
|
+
* whose `name` does not match its directory is loaded by a lenient client and
|
|
132
|
+
* SKIPPED by a strict one -- the Agent Plugins spec requires a client to skip
|
|
133
|
+
* a non-conformant skill -- so the failure shows up as a skill that quietly
|
|
134
|
+
* does nothing on somebody else's machine. This generator names the directory
|
|
135
|
+
* from the same string it writes into the frontmatter, so the pair cannot
|
|
136
|
+
* disagree; the rest is the character rules.
|
|
137
|
+
*/
|
|
138
|
+
function whyNameIsInvalid(name) {
|
|
139
|
+
if (!name)
|
|
140
|
+
return `a skill name cannot be empty.`;
|
|
141
|
+
if (name.length > 64) {
|
|
142
|
+
return `skill name '${name}' is ${name.length} characters; the maximum ` +
|
|
143
|
+
`is 64.`;
|
|
144
|
+
}
|
|
145
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
146
|
+
return `skill name '${name}' is not valid: use lowercase letters, ` +
|
|
147
|
+
`digits and single hyphens, starting and ending with a letter or a ` +
|
|
148
|
+
`digit.`;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// SKILL.md: the router.
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// The most a client reads before deciding the skill is relevant.
|
|
156
|
+
const MAX_DESCRIPTION = 1024;
|
|
157
|
+
// What the spec recommends a body stay under. Both are advisory in the sense
|
|
158
|
+
// that nothing rejects a longer one; what a longer one costs is context, on
|
|
159
|
+
// every request, whether or not the skill is used.
|
|
160
|
+
const MAX_BODY_LINES = 500;
|
|
161
|
+
const MAX_BODY_TOKENS = 5000;
|
|
162
|
+
function skillDocument(name, runtime, actions, instruction, paths) {
|
|
163
|
+
const model = runtime.model;
|
|
164
|
+
const out = [];
|
|
165
|
+
out.push('---');
|
|
166
|
+
// Quoted, though the name's alphabet is only `[a-z0-9-]`. YAML 1.1 reads
|
|
167
|
+
// `no`, `on`, `y` and an all-digit name as a boolean or an integer, so a
|
|
168
|
+
// model named `No` would emit `name: no` and load as `false` in the parsers
|
|
169
|
+
// most non-JS clients use -- a name that no longer equals its directory,
|
|
170
|
+
// which is the one failure this generator exists to make impossible.
|
|
171
|
+
out.push(`name: ${yamlString(name)}`);
|
|
172
|
+
out.push(`description: ${yamlString(descriptionFor(model, actions))}`);
|
|
173
|
+
out.push('---');
|
|
174
|
+
out.push('');
|
|
175
|
+
out.push(`# ${model.name}`);
|
|
176
|
+
out.push('');
|
|
177
|
+
if (model.description) {
|
|
178
|
+
out.push(paragraph(model.description));
|
|
179
|
+
out.push('');
|
|
180
|
+
}
|
|
181
|
+
out.push('## What you can do here');
|
|
182
|
+
out.push('');
|
|
183
|
+
if (actions.length) {
|
|
184
|
+
out.push('Each action below has a reference page with the arguments it takes, ' +
|
|
185
|
+
'the rules that gate it, and what it changes. Read the page for an ' +
|
|
186
|
+
'action before you call it.');
|
|
187
|
+
out.push('');
|
|
188
|
+
out.push('| Action | What it does | Reference |');
|
|
189
|
+
out.push('| --- | --- | --- |');
|
|
190
|
+
for (const tool of actions) {
|
|
191
|
+
// The action's OWN description, not the tool's. `toolDescription`
|
|
192
|
+
// composes description, instructions, the gating-rules block and the
|
|
193
|
+
// refusal sentence in that order, so an action that declares no
|
|
194
|
+
// description would put its instructions -- or the literal "Calling this
|
|
195
|
+
// will not work" -- into the column a client scans to pick a page.
|
|
196
|
+
const summary = firstLine(actionFor(tool, model)?.description ?? '');
|
|
197
|
+
out.push(`| \`${cell(tool.actionName)}\` | ${cell(summary || `Runs ${tool.actionName}.`)} | \`${paths.get(tool.actionName)}\` |`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
out.push(`This model declares no actions, so there is nothing here to call. ` +
|
|
202
|
+
`It describes what ${model.name} means; it does not offer a way to ` +
|
|
203
|
+
`change it.`);
|
|
204
|
+
}
|
|
205
|
+
out.push('');
|
|
206
|
+
out.push('## How this model wants to be used');
|
|
207
|
+
out.push('');
|
|
208
|
+
out.push(paragraph(instruction));
|
|
209
|
+
out.push('');
|
|
210
|
+
out.push(...readSideSection(runtime, actions));
|
|
211
|
+
out.push(...runningSection(runtime, actions));
|
|
212
|
+
out.push(...outcomeSection(actions));
|
|
213
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The line a client reads at startup to decide whether to open the skill.
|
|
217
|
+
*
|
|
218
|
+
* It has to answer "is this about my request", so it names the subject and the
|
|
219
|
+
* acts, and says plainly that this is the write side. Truncated to the spec's
|
|
220
|
+
* limit rather than allowed to run over, because a description a strict client
|
|
221
|
+
* rejects is a skill that never loads at all.
|
|
222
|
+
*/
|
|
223
|
+
const USE_WHEN = 'Use when a request asks to change this data rather than only read it.';
|
|
224
|
+
// The acts a model declares, named, in at most `budget` characters.
|
|
225
|
+
//
|
|
226
|
+
// Enough actions overrun the budget on this sentence alone. Names come off the
|
|
227
|
+
// end until it fits, rather than the whole description being cut to length,
|
|
228
|
+
// because the cut lands on whatever happens to be at 1,024 characters: the
|
|
229
|
+
// sentence saying this is the write side goes, and the list it leaves behind
|
|
230
|
+
// ends part-way through a name that does not exist. The count stays exact, so
|
|
231
|
+
// a partial list reads as one.
|
|
232
|
+
function actsSentence(actions, budget) {
|
|
233
|
+
const head = `Declares ${actions.length} action${actions.length === 1 ? '' : 's'}: `;
|
|
234
|
+
const names = actions.map(t => t.actionName);
|
|
235
|
+
let sentence = `${head}${names.join(', ')}.`;
|
|
236
|
+
for (let shown = names.length - 1; shown >= 1; shown--) {
|
|
237
|
+
if (sentence.length <= budget)
|
|
238
|
+
return sentence;
|
|
239
|
+
const dropped = `${head}${names.slice(0, shown).join(', ')}, and ${names.length - shown} more.`;
|
|
240
|
+
// Dropping a name does not always shorten the sentence: `and 1 more` is
|
|
241
|
+
// ten characters and the name it stands in for may be fewer, so with short
|
|
242
|
+
// names the abridged form runs longer than the full list. Keep whichever
|
|
243
|
+
// is shorter, or a step taken for room ends up costing it -- and the cut
|
|
244
|
+
// below would then be made in the longer of the two.
|
|
245
|
+
if (dropped.length < sentence.length)
|
|
246
|
+
sentence = dropped;
|
|
247
|
+
}
|
|
248
|
+
// Nothing left to drop and it still does not fit, so cut it and let the
|
|
249
|
+
// sentence after this one carry the routing signal.
|
|
250
|
+
return sentence.length <= budget ? sentence : truncate(sentence, budget);
|
|
251
|
+
}
|
|
252
|
+
function descriptionFor(model, actions) {
|
|
253
|
+
// What makes this line a routing decision is the part after the author's
|
|
254
|
+
// prose: which acts the model offers, and that it is the write side. So the
|
|
255
|
+
// budget is spent from the front. Truncating the joined string instead would
|
|
256
|
+
// let a long model description push both out and leave a description that
|
|
257
|
+
// reads well and no longer says what the skill is for.
|
|
258
|
+
const fixed = actions.length ?
|
|
259
|
+
`${actsSentence(actions, MAX_DESCRIPTION - USE_WHEN.length - 1)} ${USE_WHEN}` :
|
|
260
|
+
`Declares no actions; it describes the data, and changes none.`;
|
|
261
|
+
const prose = model.description?.trim() || `The ${model.name} model.`;
|
|
262
|
+
const subject = sentenceEnd(collapse(prose));
|
|
263
|
+
const room = MAX_DESCRIPTION - fixed.length - 1;
|
|
264
|
+
if (subject.length <= room)
|
|
265
|
+
return `${subject} ${fixed}`;
|
|
266
|
+
// Nothing of the subject fits: the acts are what a client routes on, so they
|
|
267
|
+
// are what survives. `fixed` is built to the limit, so it needs no cut here.
|
|
268
|
+
if (room < 8)
|
|
269
|
+
return fixed;
|
|
270
|
+
return `${truncate(subject, room)} ${fixed}`;
|
|
271
|
+
}
|
|
272
|
+
// Cut to a length in characters without splitting a surrogate pair -- a lone
|
|
273
|
+
// half is not valid UTF-8 and reaches the file as U+FFFD -- and without
|
|
274
|
+
// leaving a half-word where a space was close by.
|
|
275
|
+
function truncate(text, max) {
|
|
276
|
+
let end = max - 1;
|
|
277
|
+
const code = text.charCodeAt(end - 1);
|
|
278
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
279
|
+
end -= 1;
|
|
280
|
+
const cut = text.slice(0, end);
|
|
281
|
+
const space = cut.lastIndexOf(' ');
|
|
282
|
+
return `${(space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
283
|
+
}
|
|
284
|
+
// What a key that matches nothing costs, which is the question an agent has
|
|
285
|
+
// once it is told to go and find one. Every argument is a scalar, so there is
|
|
286
|
+
// no resolve step to get it wrong in: a wrong key reaches the statement. This
|
|
287
|
+
// runtime performs the writes of a `sql` executor itself and refuses one that
|
|
288
|
+
// reports no rows, rolling the transaction back; the kinds that hand the write
|
|
289
|
+
// to another system cannot say what that system does, so they do not say it.
|
|
290
|
+
const KEY_MATCHES_NOTHING = ' A key that matches no record costs you the call rather than the data: ' +
|
|
291
|
+
'a statement that writes no rows fails the action and rolls the whole ' +
|
|
292
|
+
'transaction back, so nothing is half-applied and nothing is silently ' +
|
|
293
|
+
'skipped. Guessing a key is therefore safe to be wrong about, and not ' +
|
|
294
|
+
'safe to be right about by accident.';
|
|
295
|
+
// What this skill does not offer, said once rather than discovered per call.
|
|
296
|
+
//
|
|
297
|
+
// The instruction above tells an agent never to invent an identifier, and this
|
|
298
|
+
// skill offers only the write side -- so where a key has to come from is named
|
|
299
|
+
// here, along with how to read the store directly when one is bound.
|
|
300
|
+
function readSideSection(runtime, actions) {
|
|
301
|
+
// Every sentence below is about supplying a key to an action, so a model
|
|
302
|
+
// with none has nothing to read a key FOR. Without this the skill says the
|
|
303
|
+
// model declares nothing to call and then closes on a shell recipe into the
|
|
304
|
+
// live store, which is the last and most concrete thing in the file.
|
|
305
|
+
if (!actions.length)
|
|
306
|
+
return [];
|
|
307
|
+
const out = [];
|
|
308
|
+
out.push('## Finding a record');
|
|
309
|
+
out.push('');
|
|
310
|
+
// The last sentence is a promise about how a write fails, and only the
|
|
311
|
+
// `sql` kind is executed by this runtime and can be promised. It is dropped
|
|
312
|
+
// rather than the section with it: the instruction above still tells an
|
|
313
|
+
// agent never to invent an identifier, and saying where a key comes from is
|
|
314
|
+
// the answer to that whichever kind performs the write.
|
|
315
|
+
const performedHere = distinctKinds(runtime.model.actions ?? []).includes('sql');
|
|
316
|
+
out.push('This skill offers writes, not reads. When you are given a name or a ' +
|
|
317
|
+
'description where an action wants a key, the key has to come from ' +
|
|
318
|
+
'somewhere else: ask the caller, or read the store directly.' +
|
|
319
|
+
(performedHere ? KEY_MATCHES_NOTHING : ''));
|
|
320
|
+
out.push('');
|
|
321
|
+
if (runtime.store?.kind === 'spanner') {
|
|
322
|
+
const s = runtime.store;
|
|
323
|
+
out.push('To read the store directly:');
|
|
324
|
+
out.push('');
|
|
325
|
+
out.push('```bash');
|
|
326
|
+
out.push(`gcloud spanner databases execute-sql ${s.database} \\`);
|
|
327
|
+
out.push(` --instance=${s.instance} --project=${s.project} \\`);
|
|
328
|
+
out.push(` --sql='SELECT ...'`);
|
|
329
|
+
out.push('```');
|
|
330
|
+
out.push('');
|
|
331
|
+
}
|
|
332
|
+
else if (runtime.store?.kind === 'bigquery') {
|
|
333
|
+
const s = runtime.store;
|
|
334
|
+
out.push('To read the store directly:');
|
|
335
|
+
out.push('');
|
|
336
|
+
out.push('```bash');
|
|
337
|
+
out.push(`bq query --use_legacy_sql=false --project_id=${s.project} --dataset_id=${s.dataset} \\`);
|
|
338
|
+
out.push(` 'SELECT ...'`);
|
|
339
|
+
out.push('```');
|
|
340
|
+
out.push('');
|
|
341
|
+
}
|
|
342
|
+
else if (runtime.store?.kind === 'alloydb') {
|
|
343
|
+
const s = runtime.store;
|
|
344
|
+
out.push(`This skill supplies no canned CLI command for AlloyDB; connect to ` +
|
|
345
|
+
`\`${s.project}/${s.location}/${s.cluster}/${s.instance}/${s.database}\` via \`psql\` or the AlloyDB Auth Proxy to run ` +
|
|
346
|
+
`\`SELECT\` queries.`);
|
|
347
|
+
out.push('');
|
|
348
|
+
}
|
|
349
|
+
if (runtime.store) {
|
|
350
|
+
out.push(...readableSchema(runtime));
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
// The tables a SELECT can name, listed rather than left to be discovered.
|
|
355
|
+
//
|
|
356
|
+
// Without this an agent knows there is a store and nothing about its shape, so
|
|
357
|
+
// it spends its first turns querying INFORMATION_SCHEMA. The model already
|
|
358
|
+
// holds the answer: the binding profile says which table each entity is and
|
|
359
|
+
// which column each field is, and `readableEntities` derives both from the
|
|
360
|
+
// profile in the store's SQL dialect (GoogleSQL or PostgreSQL).
|
|
361
|
+
function readableSchema(runtime) {
|
|
362
|
+
const dialect = (0, dialect_1.dialectFor)(runtime.store);
|
|
363
|
+
const readable = (0, agent_tools_1.readableEntities)(runtime, dialect);
|
|
364
|
+
if (!readable.length)
|
|
365
|
+
return [];
|
|
366
|
+
const out = [];
|
|
367
|
+
const hasSnippet = runtime.store?.kind === 'spanner' ||
|
|
368
|
+
runtime.store?.kind === 'bigquery';
|
|
369
|
+
const lead = hasSnippet ? `Those are ${dialect.name} statements.` :
|
|
370
|
+
`Write ${dialect.name} statements.`;
|
|
371
|
+
out.push(`${lead} These tables are the whole of ` +
|
|
372
|
+
'what there is to read, and the names to write in a statement are the ' +
|
|
373
|
+
'table and column names below -- not the model\'s own names, which ' +
|
|
374
|
+
'follow each column for cross-reference:');
|
|
375
|
+
out.push('');
|
|
376
|
+
out.push('```');
|
|
377
|
+
for (const { entity, table, fields } of readable) {
|
|
378
|
+
out.push(`${entity.name} -> table ${table}`);
|
|
379
|
+
for (const field of fields) {
|
|
380
|
+
const says = field.description?.trim();
|
|
381
|
+
out.push(` column ${dialect.quote(field.column)} (${field.type}) = ${entity.name}.${field.name}${says ? `. ${says.replace(/\s+/g, ' ')}` : ''}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
out.push('```');
|
|
385
|
+
out.push('');
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The one section that is about this deployment rather than about the model.
|
|
390
|
+
*
|
|
391
|
+
* Everything above is true of the model wherever it runs. This is the part a
|
|
392
|
+
* second profile would change, which is why it is one section and not a
|
|
393
|
+
* sentence in each action's page: regenerating against another binding
|
|
394
|
+
* rewrites here and nowhere else.
|
|
395
|
+
*/
|
|
396
|
+
function runningSection(runtime, actions) {
|
|
397
|
+
if (!actions.length)
|
|
398
|
+
return [];
|
|
399
|
+
const out = [];
|
|
400
|
+
out.push('## Running an action');
|
|
401
|
+
out.push('');
|
|
402
|
+
out.push(`Everything above is true of this model wherever it is deployed. This ` +
|
|
403
|
+
`section is not: it describes the binding this skill was generated ` +
|
|
404
|
+
`from, which is profile \`${runtime.profile}\`.`);
|
|
405
|
+
out.push('');
|
|
406
|
+
out.push(runtime.store ? `- Store: \`${(0, store_1.storeLine)(runtime.store)}\`` :
|
|
407
|
+
`- Store: none. ${runtime.storeError ?? ''}`.trim());
|
|
408
|
+
const kinds = distinctKinds(runtime.model.actions ?? []);
|
|
409
|
+
if (kinds.length) {
|
|
410
|
+
out.push(`- Executor: ${kinds.map(k => `\`${k}\``).join(', ')}`);
|
|
411
|
+
}
|
|
412
|
+
for (const tool of actions.filter(t => t.runnable)) {
|
|
413
|
+
const ex = actionFor(tool, runtime.model)?.executor;
|
|
414
|
+
if (!ex)
|
|
415
|
+
continue;
|
|
416
|
+
if (ex.kind === 'mcp') {
|
|
417
|
+
out.push(`- \`${tool.actionName}\` (\`${tool.name}\`): MCP tool \`${ex.mcp.tool}\` on \`${ex.mcp.server}\``);
|
|
418
|
+
}
|
|
419
|
+
else if (ex.kind === 'rest') {
|
|
420
|
+
out.push(`- \`${tool.actionName}\` (\`${tool.name}\`): HTTP \`${ex.rest.method}\` \`${ex.rest.endpoint}\``);
|
|
421
|
+
}
|
|
422
|
+
else if (ex.kind === 'grpc') {
|
|
423
|
+
out.push(`- \`${tool.actionName}\` (\`${tool.name}\`): gRPC \`${ex.grpc.service}/${ex.grpc.method}\``);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
out.push('');
|
|
427
|
+
// The reasons live here rather than on each action's page, and that is the
|
|
428
|
+
// whole point of this section. `tool.unavailable` is partly a fact about the
|
|
429
|
+
// binding -- a profile that binds no store leaves a `sql` action with
|
|
430
|
+
// nothing to run against -- so printing it per page would make every page
|
|
431
|
+
// change when the profile does, and the claim above would be false.
|
|
432
|
+
const blocked = actions.filter(t => !t.runnable);
|
|
433
|
+
if (blocked.length) {
|
|
434
|
+
out.push(blocked.length === actions.length ?
|
|
435
|
+
`No action in this model can be run under this profile:` :
|
|
436
|
+
`Not runnable under this profile:`);
|
|
437
|
+
out.push('');
|
|
438
|
+
for (const tool of blocked) {
|
|
439
|
+
out.push(`- \`${tool.actionName}\` -- ${sentenceEnd(collapse(tool.unavailable ?? 'no reason given'))}`);
|
|
440
|
+
}
|
|
441
|
+
out.push('');
|
|
442
|
+
}
|
|
443
|
+
if (blocked.length === actions.length) {
|
|
444
|
+
out.push('Report that rather than retrying.');
|
|
445
|
+
out.push('');
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
out.push('An agent that runs continuously should be handed these actions as ' +
|
|
449
|
+
'tools by its own framework, which puts the action\'s guards to a ' +
|
|
450
|
+
'judge before opening a transaction, and refuses rather than writing ' +
|
|
451
|
+
'unchecked when it cannot settle one the model requires.');
|
|
452
|
+
out.push('');
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* What the runtime guarantees about a call, which is what an agent has to know
|
|
457
|
+
* to report one honestly.
|
|
458
|
+
*
|
|
459
|
+
* Constant text, and deliberately so: these are facts about how actions run,
|
|
460
|
+
* not about this model. An agent that treats a refusal as a retry, or a
|
|
461
|
+
* warning as nothing, gets them wrong in the same way against every model.
|
|
462
|
+
*/
|
|
463
|
+
function outcomeSection(actions) {
|
|
464
|
+
if (!actions.length)
|
|
465
|
+
return [];
|
|
466
|
+
return [
|
|
467
|
+
'## What happens when you call one',
|
|
468
|
+
'',
|
|
469
|
+
'Every rule is settled before the write opens a transaction. So a ' +
|
|
470
|
+
'refusal leaves the store exactly as it was, and no rule ever sees ' +
|
|
471
|
+
'the write it gates. There is nothing to undo after a refusal.',
|
|
472
|
+
'',
|
|
473
|
+
'A call comes back in one of three states, and they are not two:',
|
|
474
|
+
'',
|
|
475
|
+
'- **Applied.** The write landed. Say what changed.',
|
|
476
|
+
'- **Refused.** The write did not happen, and the reason says why. ' +
|
|
477
|
+
'Repeat the reason plainly. If it says a person has to decide, say ' +
|
|
478
|
+
'so and stop -- you cannot approve it yourself, and rephrasing the ' +
|
|
479
|
+
'request to get past a rule is the one thing you must not do.',
|
|
480
|
+
'- **Unknown.** The statements ran and the commit could not report its ' +
|
|
481
|
+
'outcome. The write may or may not have landed. Do not retry: say ' +
|
|
482
|
+
'that the outcome is unknown and what to check.',
|
|
483
|
+
'',
|
|
484
|
+
'A call can also come back applied **and** carry warnings. That means ' +
|
|
485
|
+
'the change landed and a rule still went unmet, or went unchecked. ' +
|
|
486
|
+
'Report both. Reporting only the success tells the caller the write ' +
|
|
487
|
+
'met every rule the model states, which is the one thing it did not.',
|
|
488
|
+
'',
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// references/<action>.md: the detail, read on demand.
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
/**
|
|
495
|
+
* Where each action's page goes, keyed by the action's authored name.
|
|
496
|
+
*
|
|
497
|
+
* An action name is a free string: `actionSchema.name` is `z.string()` and
|
|
498
|
+
* validation checks the guards, the parameters and the blast radius but never
|
|
499
|
+
* the name's characters. So a name is not a path component. Slugging it to the
|
|
500
|
+
* same `[a-z0-9-]` alphabet the skill name uses makes a traversal structurally
|
|
501
|
+
* impossible rather than filtered for -- `../../../.bashrc` has nowhere to go
|
|
502
|
+
* once the separators and the dots are not in the alphabet.
|
|
503
|
+
*
|
|
504
|
+
* Slugging is lossy, so two names can arrive at one slug. They are numbered in
|
|
505
|
+
* declaration order, because the alternative is two actions sharing a page and
|
|
506
|
+
* the second silently overwriting the first.
|
|
507
|
+
*/
|
|
508
|
+
function referencePaths(actions) {
|
|
509
|
+
const paths = new Map();
|
|
510
|
+
const taken = new Set();
|
|
511
|
+
for (const tool of actions) {
|
|
512
|
+
const base = skillNameFor(tool.actionName) || 'action';
|
|
513
|
+
let slug = base;
|
|
514
|
+
for (let n = 2; taken.has(slug); n++)
|
|
515
|
+
slug = `${base}-${n}`;
|
|
516
|
+
taken.add(slug);
|
|
517
|
+
paths.set(tool.actionName, `references/${slug}.md`);
|
|
518
|
+
}
|
|
519
|
+
return paths;
|
|
520
|
+
}
|
|
521
|
+
function referenceDocument(tool, action, model) {
|
|
522
|
+
const out = [];
|
|
523
|
+
out.push(`# ${action.name}`);
|
|
524
|
+
out.push('');
|
|
525
|
+
// Both names, once, here. `action.name` is the authored action name in the
|
|
526
|
+
// model; `tool.name` is what the same action is called when a framework hands
|
|
527
|
+
// it over as a tool. An agent meets one or the other depending on how it was
|
|
528
|
+
// wired, and a page that showed only one would be wrong for half of them.
|
|
529
|
+
out.push(`Action \`${action.name}\` of the \`${model.name}\` model. As a ` +
|
|
530
|
+
`tool it is named \`${tool.name}\`.`);
|
|
531
|
+
out.push('');
|
|
532
|
+
if (action.description) {
|
|
533
|
+
out.push(paragraph(action.description));
|
|
534
|
+
out.push('');
|
|
535
|
+
}
|
|
536
|
+
// Whether this binding can run it is NOT here. It is a fact about the
|
|
537
|
+
// deployment, and the deployment is described in one place -- "Running an
|
|
538
|
+
// action" in SKILL.md. Repeating it per page would make every page change
|
|
539
|
+
// when the profile does, which is the property this split exists to keep.
|
|
540
|
+
out.push('## Arguments');
|
|
541
|
+
out.push('');
|
|
542
|
+
if (tool.parameters.length) {
|
|
543
|
+
const hasDefault = tool.parameters.some(p => p.default !== undefined);
|
|
544
|
+
out.push(`| Name | Type | Required |${hasDefault ? ' Default |' : ''} What to pass |`);
|
|
545
|
+
out.push(`| --- | --- | --- |${hasDefault ? ' --- |' : ''} --- |`);
|
|
546
|
+
for (const p of tool.parameters) {
|
|
547
|
+
// Escaped like every other cell: a default is an arbitrary YAML value,
|
|
548
|
+
// and a pipe in one shifts every column after it by one for the rest of
|
|
549
|
+
// the row -- on the page the skill tells an agent to read before calling.
|
|
550
|
+
const def = hasDefault ?
|
|
551
|
+
` ${p.default === undefined ?
|
|
552
|
+
'' :
|
|
553
|
+
`\`${cell(JSON.stringify(p.default))}\``} |` :
|
|
554
|
+
'';
|
|
555
|
+
out.push(`| \`${cell(p.name)}\` | ${cell(p.type ?? 'no type')} | ${p.required ? 'yes' : 'no'} |${def} ${cell(p.description)} |`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
out.push('This action takes no arguments.');
|
|
560
|
+
}
|
|
561
|
+
out.push('');
|
|
562
|
+
const instructions = action.aiContext?.instructions?.trim();
|
|
563
|
+
if (instructions) {
|
|
564
|
+
out.push('## How to call it');
|
|
565
|
+
out.push('');
|
|
566
|
+
out.push(paragraph(instructions));
|
|
567
|
+
out.push('');
|
|
568
|
+
}
|
|
569
|
+
out.push(...rulesSection(action, model));
|
|
570
|
+
out.push(...affectsSection(action));
|
|
571
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* The rules, including the ones that do not stop the call.
|
|
575
|
+
*
|
|
576
|
+
* `toolDescription` names only the gating rules, and is right to: telling a
|
|
577
|
+
* caller it is "gated" by a rule that lets every write through is a claim the
|
|
578
|
+
* runtime does not honour. A reference page is the other audience. Someone
|
|
579
|
+
* reading it wants to know what will be said about the call as well as what
|
|
580
|
+
* will stop it, so an advisory rule appears here and is marked as advisory --
|
|
581
|
+
* which is the same honesty in a place that has room for the distinction.
|
|
582
|
+
*/
|
|
583
|
+
function rulesSection(action, model) {
|
|
584
|
+
const byName = new Map((model.constraints ?? []).map(c => [c.name, c]));
|
|
585
|
+
const rules = (action.guards ?? [])
|
|
586
|
+
.map(n => byName.get(n))
|
|
587
|
+
.filter((c) => c !== undefined);
|
|
588
|
+
if (!rules.length)
|
|
589
|
+
return [];
|
|
590
|
+
const out = [];
|
|
591
|
+
out.push('## Rules that apply to this call');
|
|
592
|
+
out.push('');
|
|
593
|
+
out.push('Each is settled before anything is written, from the attempted call ' +
|
|
594
|
+
'and, where the rule is about something on record, the record.');
|
|
595
|
+
out.push('');
|
|
596
|
+
for (const rule of rules) {
|
|
597
|
+
const advisory = rule.onViolation === 'warn';
|
|
598
|
+
out.push(`### ${rule.name}${advisory ? ' (advisory)' : ''}`);
|
|
599
|
+
out.push('');
|
|
600
|
+
out.push(`On violation: \`${rule.onViolation ?? 'unspecified'}\`${advisory ? ' -- this one reports and lets the write through.' : ''}`);
|
|
601
|
+
out.push('');
|
|
602
|
+
const judgment = rule.judgment?.trim();
|
|
603
|
+
if (judgment) {
|
|
604
|
+
out.push(paragraph(judgment).split('\n').map(l => `> ${l}`).join('\n'));
|
|
605
|
+
out.push('');
|
|
606
|
+
}
|
|
607
|
+
const desc = rule.description?.trim();
|
|
608
|
+
if (desc) {
|
|
609
|
+
out.push(`If it does not hold: ${sentenceEnd(collapse(desc))}`);
|
|
610
|
+
out.push('');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
// What the call reaches, as the model declares it. Coarse on purpose where the
|
|
616
|
+
// author left it coarse: an entry with no operation says the concept is
|
|
617
|
+
// touched and does not say how, and inventing a verb for it here would make
|
|
618
|
+
// the page say more than the model does.
|
|
619
|
+
function affectsSection(action) {
|
|
620
|
+
const affects = action.affects ?? [];
|
|
621
|
+
if (!affects.length)
|
|
622
|
+
return [];
|
|
623
|
+
const out = [];
|
|
624
|
+
out.push('## What it changes');
|
|
625
|
+
out.push('');
|
|
626
|
+
out.push('| Concept | Operation | Fields |');
|
|
627
|
+
out.push('| --- | --- | --- |');
|
|
628
|
+
for (const a of affects)
|
|
629
|
+
out.push(affectsRow(a));
|
|
630
|
+
out.push('');
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
function affectsRow(a) {
|
|
634
|
+
// Concept and field names are unconstrained strings, and a pipe in one
|
|
635
|
+
// splits the row even inside backticks -- every column after it shifts by
|
|
636
|
+
// one, on a page the skill tells an agent to read before calling.
|
|
637
|
+
const op = a.operation ? `\`${cell(a.operation)}\`` : 'unspecified';
|
|
638
|
+
const fields = a.fields?.length ?
|
|
639
|
+
a.fields.map(f => `\`${cell(f)}\``).join(', ') :
|
|
640
|
+
(a.operation === 'delete' ? 'the whole record' : 'unspecified');
|
|
641
|
+
return `| \`${cell(a.concept)}\` | ${op} | ${fields} |`;
|
|
642
|
+
}
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
// Helpers.
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
function actionFor(tool, model) {
|
|
647
|
+
return (model.actions ?? []).find(a => a.name === tool.actionName);
|
|
648
|
+
}
|
|
649
|
+
function distinctKinds(actions) {
|
|
650
|
+
const kinds = new Set();
|
|
651
|
+
for (const a of actions) {
|
|
652
|
+
if (a.executor)
|
|
653
|
+
kinds.add(a.executor.kind);
|
|
654
|
+
}
|
|
655
|
+
return [...kinds].sort();
|
|
656
|
+
}
|
|
657
|
+
// The spec's budgets are about what a body costs the agent that loads it, so
|
|
658
|
+
// what is measured is the body rather than the file: frontmatter is read at
|
|
659
|
+
// startup and is accounted for by its own limits.
|
|
660
|
+
function overBudget(document) {
|
|
661
|
+
const body = document.replace(/^---\n[\s\S]*?\n---\n/, '');
|
|
662
|
+
const warnings = [];
|
|
663
|
+
const lines = body.split('\n').length;
|
|
664
|
+
if (lines > MAX_BODY_LINES) {
|
|
665
|
+
warnings.push(`SKILL.md body is ${lines} lines, over the ${MAX_BODY_LINES}-line guidance. Move detail into references/.`);
|
|
666
|
+
}
|
|
667
|
+
// Four characters to the token is the usual rough count. Approximate on
|
|
668
|
+
// purpose: the point is to notice a body that has outgrown the router shape,
|
|
669
|
+
// and being exact about it would mean shipping a tokenizer.
|
|
670
|
+
const tokens = Math.ceil(body.length / 4);
|
|
671
|
+
if (tokens > MAX_BODY_TOKENS) {
|
|
672
|
+
warnings.push(`SKILL.md body is roughly ${tokens} tokens, over the ${MAX_BODY_TOKENS}-token guidance. Move detail into references/.`);
|
|
673
|
+
}
|
|
674
|
+
return warnings;
|
|
675
|
+
}
|
|
676
|
+
/** A YAML double-quoted scalar, on one line, safe for any description. */
|
|
677
|
+
function yamlString(text) {
|
|
678
|
+
return `"${collapse(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
679
|
+
}
|
|
680
|
+
/** Folded YAML and hand-wrapped prose both arrive with newlines in them. */
|
|
681
|
+
function collapse(text) {
|
|
682
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
683
|
+
}
|
|
684
|
+
/** Markdown reads a newline inside a table cell as the end of the row. */
|
|
685
|
+
function cell(text) {
|
|
686
|
+
return collapse(text).replace(/\|/g, '\\|');
|
|
687
|
+
}
|
|
688
|
+
/** Keeps the author's paragraphs and drops their line wrapping. */
|
|
689
|
+
function paragraph(text) {
|
|
690
|
+
return text.trim().split(/\n\s*\n/).map(collapse).join('\n\n');
|
|
691
|
+
}
|
|
692
|
+
function firstLine(text) {
|
|
693
|
+
return collapse(text.split(/\n\s*\n/)[0] ?? '');
|
|
694
|
+
}
|
|
695
|
+
function sentenceEnd(text) {
|
|
696
|
+
return !text || /[.!?]$/.test(text) ? text : `${text}.`;
|
|
697
|
+
}
|