melchizedek-agents 1.0.3 → 1.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/.env.example +29 -2
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +524 -0
- package/DOCUMENTATION.md +178 -43
- package/ONBOARDING.md +339 -0
- package/QUICKSTART.md +15 -2
- package/README.md +6 -2
- package/config/agents/README.md +2 -2
- package/config/agents/examples/ares.yaml +6 -10
- package/config/agents/examples/assistant.yaml +6 -6
- package/config/agents/examples/augustin.yaml +9 -15
- package/config/agents/examples/cartographers.yaml +6 -6
- package/config/agents/examples/council.yaml +7 -7
- package/config/agents/examples/critic.yaml +24 -24
- package/config/agents/examples/harness.yaml +4 -4
- package/config/agents/examples/image_production.yaml +37 -35
- package/config/agents/examples/librarian.yaml +4 -4
- package/config/agents/examples/patient_advocate.yaml +6 -10
- package/config/agents/examples/pipeline.yaml +20 -19
- package/config/agents/examples/research.yaml +36 -43
- package/config/agents/examples/scribe.yaml +31 -32
- package/config/agents/examples/scriptorium.yaml +6 -6
- package/config/agents/examples/structured_critic.yaml +82 -0
- package/config/agents/examples/style_council.yaml +9 -11
- package/config/agents/examples/syndicate.yaml +3 -5
- package/config/agents/examples/tutor.yaml +2 -2
- package/config/agents/examples/weather.yaml +2 -2
- package/config/agents/syndicate.schema.json +859 -13
- package/config/agents/syndicateSchema.yaml +122 -73
- package/config/agents/templates/README.md +1 -1
- package/config/agents/templates/account_memory.yaml +4 -4
- package/config/agents/templates/case_desk.yaml +6 -6
- package/config/agents/templates/conversational.yaml +2 -2
- package/config/agents/templates/draft_review.yaml +6 -6
- package/config/agents/templates/intake_extractor.yaml +64 -62
- package/config/agents/templates/research_brief.yaml +6 -6
- package/config/agents/templates/research_desk.yaml +6 -6
- package/config/agents/templates/review_panel.yaml +8 -8
- package/config/agents/templates/support_triage.yaml +30 -29
- package/config/agents/templates/systems_operator.yaml +24 -5
- package/db/migrations/0014_durable_runs.sql +31 -0
- package/dist/lib/a2a/app.d.ts +27 -0
- package/dist/lib/a2a/app.js +64 -5
- package/dist/lib/a2a/executor.d.ts +7 -1
- package/dist/lib/a2a/executor.js +43 -5
- package/dist/lib/a2a/oauthSetup.d.ts +106 -0
- package/dist/lib/a2a/oauthSetup.js +259 -0
- package/dist/lib/agentDialect.d.ts +76 -0
- package/dist/lib/agentDialect.js +151 -0
- package/dist/lib/compile.d.ts +42 -6
- package/dist/lib/compile.js +97 -45
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +20 -1
- package/dist/lib/dispatch.d.ts +10 -5
- package/dist/lib/dispatch.js +10 -5
- package/dist/lib/doctor.d.ts +110 -1
- package/dist/lib/doctor.js +205 -9
- package/dist/lib/loadSyndicate.d.ts +54 -2
- package/dist/lib/loadSyndicate.js +22 -2
- package/dist/lib/memory/namespace.d.ts +6 -5
- package/dist/lib/memory/namespace.js +6 -5
- package/dist/lib/memory/providers.js +3 -4
- package/dist/lib/models/capabilities.d.ts +25 -1
- package/dist/lib/models/capabilities.js +79 -1
- package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
- package/dist/lib/models/chatCompletionsAdapter.js +19 -5
- package/dist/lib/models/claudeAdapter.js +4 -0
- package/dist/lib/models/contract.d.ts +5 -2
- package/dist/lib/models/gateway.d.ts +2 -1
- package/dist/lib/models/gateway.js +4 -2
- package/dist/lib/models/gatewayAdapter.d.ts +7 -1
- package/dist/lib/models/gatewayAdapter.js +8 -0
- package/dist/lib/models/geminiAdapter.js +8 -2
- package/dist/lib/models/genaiMapping.d.ts +8 -4
- package/dist/lib/models/genaiMapping.js +23 -9
- package/dist/lib/models/gptAdapter.d.ts +4 -2
- package/dist/lib/models/gptAdapter.js +11 -4
- package/dist/lib/models/grokAdapter.d.ts +3 -1
- package/dist/lib/models/grokAdapter.js +11 -3
- package/dist/lib/models/kimiAdapter.d.ts +2 -1
- package/dist/lib/models/kimiAdapter.js +3 -3
- package/dist/lib/models/reasoning.d.ts +30 -2
- package/dist/lib/models/reasoning.js +56 -5
- package/dist/lib/observability/tracer.d.ts +8 -1
- package/dist/lib/observability/tracer.js +33 -5
- package/dist/lib/onboarding.d.ts +134 -0
- package/dist/lib/onboarding.js +539 -0
- package/dist/lib/runtime/approvals.d.ts +8 -0
- package/dist/lib/runtime/approvals.js +4 -3
- package/dist/lib/runtime/credentials.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +23 -3
- package/dist/lib/runtime/native/agentLoop.js +107 -11
- package/dist/lib/runtime/native/checkpoint.d.ts +171 -0
- package/dist/lib/runtime/native/checkpoint.js +359 -0
- package/dist/lib/runtime/native/delegate.d.ts +222 -18
- package/dist/lib/runtime/native/delegate.js +390 -39
- package/dist/lib/runtime/native/interrupts.d.ts +184 -2
- package/dist/lib/runtime/native/interrupts.js +391 -1
- package/dist/lib/runtime/native/request.d.ts +13 -3
- package/dist/lib/runtime/native/request.js +28 -4
- package/dist/lib/runtime/native/telemetry.js +10 -15
- package/dist/lib/runtime/questions.d.ts +7 -4
- package/dist/lib/runtime/sessions.d.ts +1 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +11 -3
- package/dist/lib/runtime/syndicateTurn.js +353 -68
- package/dist/lib/runtime/turnControl.d.ts +56 -0
- package/dist/lib/runtime/turnControl.js +10 -0
- package/dist/lib/session/transcript.d.ts +9 -0
- package/dist/lib/session/transcript.js +20 -1
- package/dist/lib/skills.d.ts +18 -0
- package/dist/lib/skills.js +73 -1
- package/dist/lib/storage/postgres/index.d.ts +12 -2
- package/dist/lib/storage/postgres/index.js +8 -3
- package/dist/lib/storage/postgres/taskQueue.d.ts +20 -4
- package/dist/lib/storage/postgres/taskQueue.js +29 -5
- package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
- package/dist/lib/storage/postgres/taskStore.js +74 -8
- package/dist/lib/syndicateSchema.d.ts +316 -1
- package/dist/lib/syndicateSchema.js +203 -29
- package/dist/lib/tools/auth.d.ts +20 -6
- package/dist/lib/tools/auth.js +21 -5
- package/dist/lib/tools/credentialEnv.d.ts +28 -0
- package/dist/lib/tools/credentialEnv.js +52 -0
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialStore.js +9 -2
- package/dist/lib/tools/credentialUses.d.ts +92 -0
- package/dist/lib/tools/credentialUses.js +112 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
- package/dist/lib/tools/mcpToolFactory.js +179 -21
- package/dist/lib/tools/oauthConsent.js +8 -1
- package/dist/lib/tools/oauthHosts.d.ts +73 -0
- package/dist/lib/tools/oauthHosts.js +154 -0
- package/dist/lib/tools/oauthTools.d.ts +242 -0
- package/dist/lib/tools/oauthTools.js +447 -0
- package/dist/lib/tools/openapiTools.d.ts +11 -6
- package/dist/lib/tools/openapiTools.js +88 -38
- package/dist/lib/tools/taskTools.d.ts +61 -6
- package/dist/lib/tools/taskTools.js +145 -21
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +14 -7
- package/dist/lib/workflow.js +14 -7
- package/dist/lib/workflowConfig.d.ts +6 -0
- package/dist/scripts/a2a_server.d.ts +36 -0
- package/dist/scripts/a2a_server.js +131 -1
- package/dist/scripts/assistant_worker.d.ts +12 -0
- package/dist/scripts/assistant_worker.js +81 -22
- package/dist/scripts/doctor.d.ts +7 -2
- package/dist/scripts/doctor.js +9 -4
- package/dist/scripts/setup.d.ts +23 -0
- package/dist/scripts/setup.js +154 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/yaml_codemod.d.ts +62 -0
- package/dist/scripts/yaml_codemod.js +652 -0
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +7 -5
- package/skills/briefs/melchizedek-models.md +7 -4
- package/skills/briefs/melchizedek.md +1 -0
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek/SKILL.md +1 -0
- package/skills/melchizedek-author/SKILL.md +7 -4
- package/skills/melchizedek-author/assets/minimal.yaml +13 -13
- package/skills/melchizedek-models/SKILL.md +11 -4
- package/skills/melchizedek-onboard/SKILL.md +49 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/yaml_codemod.ts — the `melchizedek-codemod` bin and `npm run yaml:codemod`.
|
|
4
|
+
*
|
|
5
|
+
* Rewrites an agent's v1 spellings into the v2 keys (ADR 0115):
|
|
6
|
+
*
|
|
7
|
+
* npm run yaml:codemod -- config/agents/my_syndicate.yaml # rewrite in place
|
|
8
|
+
* npm run yaml:codemod -- config/agents # every *.yaml below it
|
|
9
|
+
* npm run yaml:codemod -- --check config/agents # exit 1 if any file would change
|
|
10
|
+
*
|
|
11
|
+
* Per agent (the orchestrator and every inline subagent):
|
|
12
|
+
*
|
|
13
|
+
* outputSchema → output.schema
|
|
14
|
+
* generateContentConfig.responseMimeType → output.mime (application/json | text/plain)
|
|
15
|
+
* generateContentConfig.temperature → sampling.temperature
|
|
16
|
+
* generateContentConfig.topP → sampling.top_p
|
|
17
|
+
* generateContentConfig.maxOutputTokens → sampling.max_output_tokens
|
|
18
|
+
* generateContentConfig.stopSequences → sampling.stop
|
|
19
|
+
* generateContentConfig.thinkingConfig / reasoningEffort → reasoning:, ONLY
|
|
20
|
+
* when the engine reads the same ReasoningSetting before and after for
|
|
21
|
+
* the agent's model (reasoningOf over the old config equals reasoningOf
|
|
22
|
+
* over what reasoningConfig makes of the new key). `xhigh` and `max`
|
|
23
|
+
* included (ADR 0117).
|
|
24
|
+
* generateContentConfig.thinkingConfig.includeThoughts → dropped: the
|
|
25
|
+
* engine reads it nowhere, and the Gemini adapter asks for the thought
|
|
26
|
+
* trace itself whenever reasoning is not `none` (ADR 0117); `true` gets a
|
|
27
|
+
* note saying so.
|
|
28
|
+
* generateContentConfig.toolConfig → tool_choice: (ADR 0117), when it holds
|
|
29
|
+
* only functionCallingConfig (mode AUTO, NONE or ANY, and
|
|
30
|
+
* allowedFunctionNames) and includeServerSideToolInvocations, which the
|
|
31
|
+
* compiler sets on every agent: AUTO is `auto`, NONE `none`, ANY with
|
|
32
|
+
* one name `{ name }`, and ANY with none or several `required`, which is
|
|
33
|
+
* what the engine reads (genaiMapping toolChoiceOf) either way.
|
|
34
|
+
*
|
|
35
|
+
* Anything else stays under generateContentConfig with a note naming its key
|
|
36
|
+
* path (never its value); an emptied generateContentConfig is removed. An
|
|
37
|
+
* agent that already has the v2 key for a concept is left alone for that
|
|
38
|
+
* concept, with a note: the loader refuses an overlap.
|
|
39
|
+
*
|
|
40
|
+
* HOW: the file is parsed with the `yaml` package's Document API, which
|
|
41
|
+
* locates every key and value in the source. The edit splices whole lines:
|
|
42
|
+
* a moved key travels with its own lines (its leading and inline comments,
|
|
43
|
+
* its indentation and quoting); the comments of a key that is converted or
|
|
44
|
+
* dropped are kept as comment lines where the new key goes. Every line the
|
|
45
|
+
* codemod does not move is left byte for byte. The result is parsed again and
|
|
46
|
+
* must read back as the planned data, or the file is refused. Running it
|
|
47
|
+
* twice changes nothing the second time.
|
|
48
|
+
*/
|
|
49
|
+
import { readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
50
|
+
import { join } from 'node:path';
|
|
51
|
+
import { pathToFileURL } from 'node:url';
|
|
52
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
53
|
+
import { isMap, isPair, isScalar, isSeq, parseDocument } from 'yaml';
|
|
54
|
+
import { functionCallingConfigOf } from '../lib/agentDialect.js';
|
|
55
|
+
import { reasoningOf, toolChoiceOf } from '../lib/models/genaiMapping.js';
|
|
56
|
+
import { reasoningConfig } from '../lib/models/reasoning.js';
|
|
57
|
+
/** generateContentConfig keys with a `sampling:` form, and the check its value must pass. */
|
|
58
|
+
const SAMPLING = new Map([
|
|
59
|
+
['temperature', { key: 'temperature', ok: (v) => typeof v === 'number' && Number.isFinite(v) }],
|
|
60
|
+
['topP', { key: 'top_p', ok: (v) => typeof v === 'number' && Number.isFinite(v) }],
|
|
61
|
+
['maxOutputTokens', { key: 'max_output_tokens', ok: (v) => typeof v === 'number' && Number.isInteger(v) && v > 0 }],
|
|
62
|
+
['stopSequences', { key: 'stop', ok: (v) => Array.isArray(v) && v.length > 0 && v.every((s) => typeof s === 'string') }],
|
|
63
|
+
]);
|
|
64
|
+
const OUTPUT_MIMES = new Set(['application/json', 'text/plain']);
|
|
65
|
+
const THINKING_KEYS = new Set(['thinkingLevel', 'thinkingBudget', 'includeThoughts']);
|
|
66
|
+
/** toolConfig keys the codemod can account for: the choice, and the flag the compiler sets on every agent. */
|
|
67
|
+
const TOOL_CONFIG_KEYS = new Set(['functionCallingConfig', 'includeServerSideToolInvocations']);
|
|
68
|
+
const FUNCTION_CALLING_KEYS = new Set(['mode', 'allowedFunctionNames']);
|
|
69
|
+
const FUNCTION_CALLING_MODES = new Set(['AUTO', 'NONE', 'ANY']);
|
|
70
|
+
// ── The source, by line ──────────────────────────────────────────────────────
|
|
71
|
+
class Source {
|
|
72
|
+
text;
|
|
73
|
+
eol;
|
|
74
|
+
lines;
|
|
75
|
+
starts;
|
|
76
|
+
constructor(text) {
|
|
77
|
+
this.text = text;
|
|
78
|
+
this.eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
79
|
+
const raw = text.split('\n');
|
|
80
|
+
this.starts = [];
|
|
81
|
+
let at = 0;
|
|
82
|
+
for (const line of raw) {
|
|
83
|
+
this.starts.push(at);
|
|
84
|
+
at += line.length + 1;
|
|
85
|
+
}
|
|
86
|
+
this.lines = raw.map((l) => (l.endsWith('\r') ? l.slice(0, -1) : l));
|
|
87
|
+
}
|
|
88
|
+
lineOf(offset) {
|
|
89
|
+
let lo = 0;
|
|
90
|
+
let hi = this.starts.length - 1;
|
|
91
|
+
while (lo < hi) {
|
|
92
|
+
const mid = (lo + hi + 1) >> 1;
|
|
93
|
+
if (this.starts[mid] <= offset)
|
|
94
|
+
lo = mid;
|
|
95
|
+
else
|
|
96
|
+
hi = mid - 1;
|
|
97
|
+
}
|
|
98
|
+
return lo;
|
|
99
|
+
}
|
|
100
|
+
col(offset) {
|
|
101
|
+
return offset - this.starts[this.lineOf(offset)];
|
|
102
|
+
}
|
|
103
|
+
/** The line of a node's last non-blank character (a trailing comment or blank line is not the node's). */
|
|
104
|
+
endLineOf(range, fallback) {
|
|
105
|
+
if (!range || range[1] <= range[0])
|
|
106
|
+
return fallback;
|
|
107
|
+
let i = range[1] - 1;
|
|
108
|
+
while (i > range[0] && isSpace(this.text.charCodeAt(i)))
|
|
109
|
+
i--;
|
|
110
|
+
return Math.max(fallback, this.lineOf(i));
|
|
111
|
+
}
|
|
112
|
+
/** The `# ...` comment that follows `offset` on its line (after an optional `:`), if any. */
|
|
113
|
+
commentAt(offset) {
|
|
114
|
+
const line = this.lineOf(offset);
|
|
115
|
+
const text = this.lines[line];
|
|
116
|
+
let i = offset - this.starts[line];
|
|
117
|
+
const skip = () => {
|
|
118
|
+
while (i < text.length && (text[i] === ' ' || text[i] === '\t'))
|
|
119
|
+
i++;
|
|
120
|
+
};
|
|
121
|
+
skip();
|
|
122
|
+
if (text[i] === ':') {
|
|
123
|
+
i++;
|
|
124
|
+
skip();
|
|
125
|
+
}
|
|
126
|
+
return text[i] === '#' ? text.slice(i).trimEnd() : undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isSpace(code) {
|
|
130
|
+
return code === 32 || code === 9 || code === 10 || code === 13;
|
|
131
|
+
}
|
|
132
|
+
const keyName = (pair) => (isScalar(pair.key) && typeof pair.key.value === 'string' ? pair.key.value : undefined);
|
|
133
|
+
const findPair = (map, key) => map.items.find((p) => keyName(p) === key);
|
|
134
|
+
const keyRange = (pair) => pair.key.range;
|
|
135
|
+
const valueRange = (pair) => pair.value?.range;
|
|
136
|
+
/** The pair's own lines: its key line through its value's last line. */
|
|
137
|
+
function pairSpan(src, pair) {
|
|
138
|
+
const keyLine = src.lineOf(keyRange(pair)[0]);
|
|
139
|
+
return [keyLine, src.endLineOf(valueRange(pair), keyLine)];
|
|
140
|
+
}
|
|
141
|
+
/** Every comment in lines [from, to] of a subtree: full-line comments, and inline ones after keys and values. */
|
|
142
|
+
function commentsIn(src, from, to, root) {
|
|
143
|
+
const byLine = new Map();
|
|
144
|
+
for (let l = from; l <= to; l++) {
|
|
145
|
+
const t = src.lines[l].trim();
|
|
146
|
+
if (t.startsWith('#'))
|
|
147
|
+
byLine.set(l, t);
|
|
148
|
+
}
|
|
149
|
+
const inline = (offset) => {
|
|
150
|
+
if (offset === undefined)
|
|
151
|
+
return;
|
|
152
|
+
const line = src.lineOf(offset);
|
|
153
|
+
if (byLine.has(line))
|
|
154
|
+
return;
|
|
155
|
+
const c = src.commentAt(offset);
|
|
156
|
+
if (c)
|
|
157
|
+
byLine.set(line, c);
|
|
158
|
+
};
|
|
159
|
+
const walk = (node) => {
|
|
160
|
+
if (isPair(node)) {
|
|
161
|
+
inline(node.key?.range?.[1]);
|
|
162
|
+
walk(node.value);
|
|
163
|
+
}
|
|
164
|
+
else if (isMap(node) || isSeq(node)) {
|
|
165
|
+
if (node.flow)
|
|
166
|
+
inline(node.range?.[1]);
|
|
167
|
+
for (const item of node.items)
|
|
168
|
+
walk(item);
|
|
169
|
+
}
|
|
170
|
+
else if (isScalar(node)) {
|
|
171
|
+
inline(node.range?.[1]);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
walk(root);
|
|
175
|
+
return [...byLine.entries()].sort((a, b) => a[0] - b[0]).map(([, c]) => c);
|
|
176
|
+
}
|
|
177
|
+
function reindent(lines, delta) {
|
|
178
|
+
if (delta === 0)
|
|
179
|
+
return lines;
|
|
180
|
+
return lines.map((l) => {
|
|
181
|
+
if (l.length === 0)
|
|
182
|
+
return l;
|
|
183
|
+
if (delta > 0)
|
|
184
|
+
return ' '.repeat(delta) + l;
|
|
185
|
+
let n = 0;
|
|
186
|
+
while (n < -delta && l[n] === ' ')
|
|
187
|
+
n++;
|
|
188
|
+
return l.slice(n);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function scalarString(map, key) {
|
|
192
|
+
const p = findPair(map, key);
|
|
193
|
+
return p && isScalar(p.value) && typeof p.value.value === 'string' ? p.value.value : undefined;
|
|
194
|
+
}
|
|
195
|
+
function planAgent(doc, src, map, path, notes) {
|
|
196
|
+
const gccPair = findPair(map, 'generateContentConfig');
|
|
197
|
+
const osPair = findPair(map, 'outputSchema');
|
|
198
|
+
if (!gccPair && !osPair)
|
|
199
|
+
return undefined;
|
|
200
|
+
const has = (k) => findPair(map, k) !== undefined;
|
|
201
|
+
const plan = { path, map, sampling: [] };
|
|
202
|
+
const gccPath = `${path}.generateContentConfig`;
|
|
203
|
+
if (has('yaml_reference') || has('a2a_agent_url')) {
|
|
204
|
+
if (gccPair)
|
|
205
|
+
notes.push(`${gccPath}: a yaml_reference or a2a_agent_url entry brings its own config; left in place`);
|
|
206
|
+
if (osPair)
|
|
207
|
+
notes.push(`${path}.outputSchema: a yaml_reference or a2a_agent_url entry brings its own config; left in place`);
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
// A key at the head of a list item shares its line with `- `: rewriting
|
|
211
|
+
// that line is not worth the risk, and no shipped file does it.
|
|
212
|
+
for (const pair of [gccPair, osPair]) {
|
|
213
|
+
if (!pair)
|
|
214
|
+
continue;
|
|
215
|
+
const start = keyRange(pair)[0];
|
|
216
|
+
if (src.lines[src.lineOf(start)].slice(0, src.col(start)).trim() !== '') {
|
|
217
|
+
notes.push(`${path}.${keyName(pair)}: the first key of its list item; move it by hand`);
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (osPair) {
|
|
222
|
+
if (has('output'))
|
|
223
|
+
notes.push(`${path}.outputSchema: ${path}.output is already set; left in place (the loader refuses both)`);
|
|
224
|
+
else if (!isMap(osPair.value))
|
|
225
|
+
notes.push(`${path}.outputSchema: not a mapping, so no output.schema form; left in place`);
|
|
226
|
+
else
|
|
227
|
+
plan.schema = osPair;
|
|
228
|
+
}
|
|
229
|
+
if (gccPair) {
|
|
230
|
+
const gcc = gccPair.value;
|
|
231
|
+
if (!isMap(gcc) || gcc.flow || gcc.items.length === 0) {
|
|
232
|
+
notes.push(`${gccPath}: not a block mapping; left in place, move it by hand`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const keyLine = src.lineOf(keyRange(gccPair)[0]);
|
|
236
|
+
const chunks = [];
|
|
237
|
+
let prev = keyLine;
|
|
238
|
+
for (const item of gcc.items) {
|
|
239
|
+
const [, to] = pairSpan(src, item);
|
|
240
|
+
chunks.push({ pair: item, name: keyName(item) ?? '', from: prev + 1, to });
|
|
241
|
+
prev = to;
|
|
242
|
+
}
|
|
243
|
+
plan.gcc = { pair: gccPair, chunks, keyLine, indent: src.col(keyRange(gccPair)[0]), childIndent: src.col(keyRange(chunks[0].pair)[0]) };
|
|
244
|
+
const valueOf = (c) => c.pair.value?.toJS?.(doc) ?? c.pair.value;
|
|
245
|
+
// Sampling.
|
|
246
|
+
for (const c of chunks) {
|
|
247
|
+
const spec = SAMPLING.get(c.name);
|
|
248
|
+
if (!spec)
|
|
249
|
+
continue;
|
|
250
|
+
if (!spec.ok(valueOf(c)))
|
|
251
|
+
notes.push(`${gccPath}.${c.name}: this value has no sampling.${spec.key} form; left in place`);
|
|
252
|
+
else if (has('sampling'))
|
|
253
|
+
notes.push(`${gccPath}.${c.name}: ${path}.sampling is already set; left in place (move it by hand)`);
|
|
254
|
+
else
|
|
255
|
+
plan.sampling.push(c);
|
|
256
|
+
}
|
|
257
|
+
// Output mime.
|
|
258
|
+
const mime = chunks.find((c) => c.name === 'responseMimeType');
|
|
259
|
+
if (mime) {
|
|
260
|
+
if (!OUTPUT_MIMES.has(valueOf(mime)))
|
|
261
|
+
notes.push(`${gccPath}.responseMimeType: no output.mime form for this value; left in place`);
|
|
262
|
+
else if (has('output'))
|
|
263
|
+
notes.push(`${gccPath}.responseMimeType: ${path}.output is already set; left in place (move it by hand)`);
|
|
264
|
+
else
|
|
265
|
+
plan.mime = mime;
|
|
266
|
+
}
|
|
267
|
+
// Thinking.
|
|
268
|
+
const tc = chunks.find((c) => c.name === 'thinkingConfig');
|
|
269
|
+
const re = chunks.find((c) => c.name === 'reasoningEffort');
|
|
270
|
+
if (tc || re)
|
|
271
|
+
planThinking(plan, map, tc, re, valueOf, notes);
|
|
272
|
+
// Tool choice.
|
|
273
|
+
const toolConfig = chunks.find((c) => c.name === 'toolConfig');
|
|
274
|
+
if (toolConfig)
|
|
275
|
+
planToolChoice(plan, map, toolConfig, valueOf(toolConfig), notes);
|
|
276
|
+
// Leftovers: everything not moved, converted or dropped.
|
|
277
|
+
const handled = new Set([...plan.sampling, ...(plan.mime ? [plan.mime] : []), ...(plan.reasoning?.remove ?? [])]);
|
|
278
|
+
if (plan.dropInclude?.empties)
|
|
279
|
+
handled.add(plan.dropInclude.tc);
|
|
280
|
+
if (plan.toolChoice)
|
|
281
|
+
handled.add(plan.toolChoice.chunk);
|
|
282
|
+
const noted = new Set(['thinkingConfig', 'reasoningEffort', 'responseMimeType', 'toolConfig', ...SAMPLING.keys()]);
|
|
283
|
+
for (const c of chunks) {
|
|
284
|
+
if (handled.has(c) || noted.has(c.name))
|
|
285
|
+
continue;
|
|
286
|
+
notes.push(`${gccPath}.${c.name || '(non-string key)'}: no v2 form; left under generateContentConfig`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const changes = plan.sampling.length > 0 || plan.mime || plan.schema || plan.reasoning || plan.dropInclude || plan.toolChoice;
|
|
291
|
+
return changes ? plan : undefined;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* toolConfig → tool_choice, when the engine reads the same ToolChoice before
|
|
295
|
+
* and after (genaiMapping toolChoiceOf) and nothing else in it would be lost.
|
|
296
|
+
*/
|
|
297
|
+
function planToolChoice(plan, map, chunk, value, notes) {
|
|
298
|
+
const where = `${plan.path}.generateContentConfig.toolConfig`;
|
|
299
|
+
const leave = (why) => {
|
|
300
|
+
notes.push(`${where}: ${why}; left in place`);
|
|
301
|
+
};
|
|
302
|
+
if (findPair(map, 'tool_choice'))
|
|
303
|
+
return leave(`${plan.path}.tool_choice is already set (the loader refuses both)`);
|
|
304
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
305
|
+
return leave('not a mapping');
|
|
306
|
+
const tc = value;
|
|
307
|
+
if (Object.keys(tc).some((k) => !TOOL_CONFIG_KEYS.has(k)))
|
|
308
|
+
return leave('it holds a key with no v2 form');
|
|
309
|
+
const fcc = tc.functionCallingConfig;
|
|
310
|
+
if (fcc !== undefined) {
|
|
311
|
+
if (!fcc || typeof fcc !== 'object' || Array.isArray(fcc))
|
|
312
|
+
return leave('functionCallingConfig is not a mapping');
|
|
313
|
+
const f = fcc;
|
|
314
|
+
if (Object.keys(f).some((k) => !FUNCTION_CALLING_KEYS.has(k)))
|
|
315
|
+
return leave('functionCallingConfig holds a key with no tool_choice form');
|
|
316
|
+
if (f.mode === 'VALIDATED')
|
|
317
|
+
return leave('mode VALIDATED (strict tool schemas) has no tool_choice form');
|
|
318
|
+
if (!FUNCTION_CALLING_MODES.has(f.mode))
|
|
319
|
+
return leave('its mode is not AUTO, NONE or ANY');
|
|
320
|
+
const names = f.allowedFunctionNames;
|
|
321
|
+
if (names !== undefined && !(Array.isArray(names) && names.every((n) => typeof n === 'string')))
|
|
322
|
+
return leave('allowedFunctionNames is not a list of names');
|
|
323
|
+
if (f.mode === 'ANY' && Array.isArray(names) && names.length > 1) {
|
|
324
|
+
notes.push(`${where}.functionCallingConfig.allowedFunctionNames: several names, which the engine sends as required (any tool); tool_choice: required says the same`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// What the engine reads today is the v2 setting; the proof is that the v2
|
|
328
|
+
// key's engine form reads back the same.
|
|
329
|
+
const before = toolChoiceOf({ toolConfig: tc });
|
|
330
|
+
const setting = before.toolChoice ?? 'auto';
|
|
331
|
+
const after = toolChoiceOf({ toolConfig: { functionCallingConfig: functionCallingConfigOf(setting) } });
|
|
332
|
+
if (!isDeepStrictEqual(before, after))
|
|
333
|
+
return leave('tool_choice: would change what the engine reads');
|
|
334
|
+
plan.toolChoice = { setting, chunk };
|
|
335
|
+
}
|
|
336
|
+
function planThinking(plan, map, tc, re, valueOf, notes) {
|
|
337
|
+
const gccPath = `${plan.path}.generateContentConfig`;
|
|
338
|
+
const where = [tc, re].filter((c) => !!c).map((c) => `${gccPath}.${c.name}`).join(', ');
|
|
339
|
+
const tcValue = tc ? valueOf(tc) : undefined;
|
|
340
|
+
const tcObject = tcValue && typeof tcValue === 'object' && !Array.isArray(tcValue) ? tcValue : undefined;
|
|
341
|
+
const model = scalarString(map, 'model');
|
|
342
|
+
// includeThoughts is read by nothing: the Gemini adapter asks for the
|
|
343
|
+
// thought trace whenever reasoning is not `none` (ADR 0117). Drop it from
|
|
344
|
+
// a block thinkingConfig whatever else happens to the thinking keys.
|
|
345
|
+
const incPair = tc && isMap(tc.pair.value) && !tc.pair.value.flow ? findPair(tc.pair.value, 'includeThoughts') : undefined;
|
|
346
|
+
const incValue = incPair && isScalar(incPair.value) ? incPair.value.value : undefined;
|
|
347
|
+
const incBool = typeof incValue === 'boolean';
|
|
348
|
+
if (incValue === true) {
|
|
349
|
+
notes.push(`${gccPath}.thinkingConfig.includeThoughts: read by nothing (the Gemini adapter asks for the thought trace whenever reasoning is not none); dropped`);
|
|
350
|
+
}
|
|
351
|
+
const dropInclude = () => {
|
|
352
|
+
if (tc && incBool)
|
|
353
|
+
plan.dropInclude = { tc, inc: incPair, empties: tc.pair.value.items.length === 1 };
|
|
354
|
+
};
|
|
355
|
+
const leave = (why) => {
|
|
356
|
+
notes.push(`${where}: ${why}; left in place`);
|
|
357
|
+
dropInclude();
|
|
358
|
+
};
|
|
359
|
+
if (findPair(map, 'reasoning'))
|
|
360
|
+
return leave(`${plan.path}.reasoning is already set (the loader refuses both)`);
|
|
361
|
+
if (!model || model.includes('{{'))
|
|
362
|
+
return leave('the agent names no model id, so no reasoning: form can be checked');
|
|
363
|
+
if (tc && !tcObject)
|
|
364
|
+
return leave('thinkingConfig is not a mapping');
|
|
365
|
+
if (tcObject && Object.keys(tcObject).some((k) => !THINKING_KEYS.has(k)))
|
|
366
|
+
return leave('thinkingConfig holds a key with no reasoning: form');
|
|
367
|
+
const v1 = {};
|
|
368
|
+
if (tc)
|
|
369
|
+
v1.thinkingConfig = tcValue;
|
|
370
|
+
if (re)
|
|
371
|
+
v1.reasoningEffort = valueOf(re);
|
|
372
|
+
// The setting is what the engine reads today; it converts when the v2
|
|
373
|
+
// key's engine form reads back the same for the agent's model.
|
|
374
|
+
const before = reasoningOf(v1);
|
|
375
|
+
if (before === undefined) {
|
|
376
|
+
// A thinkingConfig holding includeThoughts alone: nothing to convert, and nothing left.
|
|
377
|
+
if (!re && tcObject && Object.keys(tcObject).every((k) => k === 'includeThoughts'))
|
|
378
|
+
return dropInclude();
|
|
379
|
+
return leave('no reasoning: setting reads the same');
|
|
380
|
+
}
|
|
381
|
+
const after = reasoningOf(reasoningConfig(model, before));
|
|
382
|
+
if (!isDeepStrictEqual(before, after))
|
|
383
|
+
return leave(`reasoning: would change what the engine reads for ${providerWord(model)}`);
|
|
384
|
+
plan.reasoning = { setting: before, remove: [tc, re].filter((c) => !!c) };
|
|
385
|
+
}
|
|
386
|
+
/** A model family word for a note: never the id's value beyond its prefix. */
|
|
387
|
+
function providerWord(model) {
|
|
388
|
+
return model.startsWith('gemini-') ? 'this Gemini model' : 'this model';
|
|
389
|
+
}
|
|
390
|
+
const reasoningText = (s) => (typeof s === 'string' ? s : `{ budget_tokens: ${s.budget_tokens} }`);
|
|
391
|
+
/** A tool name YAML reads back as that string unquoted: an identifier that is not a YAML keyword. */
|
|
392
|
+
const PLAIN_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
393
|
+
const YAML_WORDS = new Set(['true', 'false', 'null', 'yes', 'no', 'on', 'off', 'y', 'n']);
|
|
394
|
+
const toolChoiceText = (s) => {
|
|
395
|
+
if (typeof s === 'string')
|
|
396
|
+
return s;
|
|
397
|
+
const plain = PLAIN_NAME.test(s.name) && !YAML_WORDS.has(s.name.toLowerCase());
|
|
398
|
+
return `{ name: ${plain ? s.name : JSON.stringify(s.name)} }`;
|
|
399
|
+
};
|
|
400
|
+
function chunkLines(src, c) {
|
|
401
|
+
return src.lines.slice(c.from, c.to + 1);
|
|
402
|
+
}
|
|
403
|
+
/** A chunk with its key token renamed (same line, same column). */
|
|
404
|
+
function renamed(src, c, key) {
|
|
405
|
+
const lines = chunkLines(src, c);
|
|
406
|
+
const [start, end] = keyRange(c.pair);
|
|
407
|
+
const at = src.lineOf(start) - c.from;
|
|
408
|
+
const line = lines[at];
|
|
409
|
+
lines[at] = line.slice(0, src.col(start)) + key + line.slice(src.col(end));
|
|
410
|
+
return lines;
|
|
411
|
+
}
|
|
412
|
+
function renderGcc(src, plan) {
|
|
413
|
+
const g = plan.gcc;
|
|
414
|
+
if (!g)
|
|
415
|
+
return undefined;
|
|
416
|
+
if (!(plan.sampling.length || plan.mime || plan.reasoning || plan.dropInclude || plan.toolChoice))
|
|
417
|
+
return undefined;
|
|
418
|
+
const pad = ' '.repeat(g.indent);
|
|
419
|
+
const removed = new Set([...plan.sampling, ...(plan.mime ? [plan.mime] : []), ...(plan.reasoning?.remove ?? [])]);
|
|
420
|
+
if (plan.dropInclude?.empties)
|
|
421
|
+
removed.add(plan.dropInclude.tc);
|
|
422
|
+
if (plan.toolChoice)
|
|
423
|
+
removed.add(plan.toolChoice.chunk);
|
|
424
|
+
const leftovers = g.chunks.filter((c) => !removed.has(c));
|
|
425
|
+
const out = [];
|
|
426
|
+
if (leftovers.length === 0) {
|
|
427
|
+
const c = src.commentAt(keyRange(g.pair)[1]);
|
|
428
|
+
if (c)
|
|
429
|
+
out.push(pad + c);
|
|
430
|
+
}
|
|
431
|
+
if (plan.sampling.length) {
|
|
432
|
+
out.push(`${pad}sampling:`);
|
|
433
|
+
for (const c of plan.sampling)
|
|
434
|
+
out.push(...renamed(src, c, SAMPLING.get(c.name).key));
|
|
435
|
+
}
|
|
436
|
+
if (plan.reasoning) {
|
|
437
|
+
for (const c of plan.reasoning.remove)
|
|
438
|
+
for (const comment of commentsIn(src, c.from, c.to, c.pair))
|
|
439
|
+
out.push(pad + comment);
|
|
440
|
+
out.push(`${pad}reasoning: ${reasoningText(plan.reasoning.setting)}`);
|
|
441
|
+
}
|
|
442
|
+
if (plan.toolChoice) {
|
|
443
|
+
const c = plan.toolChoice.chunk;
|
|
444
|
+
for (const comment of commentsIn(src, c.from, c.to, c.pair))
|
|
445
|
+
out.push(pad + comment);
|
|
446
|
+
out.push(`${pad}tool_choice: ${toolChoiceText(plan.toolChoice.setting)}`);
|
|
447
|
+
}
|
|
448
|
+
if (plan.mime && !plan.schema) {
|
|
449
|
+
out.push(`${pad}output:`);
|
|
450
|
+
out.push(...renamed(src, plan.mime, 'mime'));
|
|
451
|
+
}
|
|
452
|
+
const orphans = plan.dropInclude?.empties ? commentsIn(src, plan.dropInclude.tc.from, plan.dropInclude.tc.to, plan.dropInclude.tc.pair) : [];
|
|
453
|
+
if (leftovers.length) {
|
|
454
|
+
out.push(src.lines[g.keyLine]);
|
|
455
|
+
for (const c of leftovers) {
|
|
456
|
+
if (plan.dropInclude && c === plan.dropInclude.tc)
|
|
457
|
+
out.push(...withoutPair(src, c, plan.dropInclude.inc));
|
|
458
|
+
else
|
|
459
|
+
out.push(...chunkLines(src, c));
|
|
460
|
+
}
|
|
461
|
+
for (const comment of orphans)
|
|
462
|
+
out.push(' '.repeat(g.childIndent) + comment);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
for (const comment of orphans)
|
|
466
|
+
out.push(pad + comment);
|
|
467
|
+
}
|
|
468
|
+
return { from: g.keyLine, to: g.chunks[g.chunks.length - 1].to, lines: out };
|
|
469
|
+
}
|
|
470
|
+
/** A chunk with one nested pair's own lines removed, its inline comments kept as comment lines. */
|
|
471
|
+
function withoutPair(src, c, inner) {
|
|
472
|
+
const [from, to] = pairSpan(src, inner);
|
|
473
|
+
const indent = ' '.repeat(src.col(keyRange(inner)[0]));
|
|
474
|
+
const comments = commentsIn(src, from, to, inner).map((t) => indent + t);
|
|
475
|
+
return [...src.lines.slice(c.from, from), ...comments, ...src.lines.slice(to + 1, c.to + 1)];
|
|
476
|
+
}
|
|
477
|
+
function renderSchema(src, plan) {
|
|
478
|
+
const os = plan.schema;
|
|
479
|
+
if (!os)
|
|
480
|
+
return undefined;
|
|
481
|
+
const [keyStart, keyEnd] = keyRange(os);
|
|
482
|
+
const indent = src.col(keyStart);
|
|
483
|
+
const [from, to] = pairSpan(src, os);
|
|
484
|
+
const value = os.value;
|
|
485
|
+
const first = value.items[0];
|
|
486
|
+
const step = !value.flow && first && src.lineOf(keyRange(first)[0]) > from
|
|
487
|
+
? src.col(keyRange(first)[0]) - indent
|
|
488
|
+
: plan.gcc
|
|
489
|
+
? plan.gcc.childIndent - plan.gcc.indent
|
|
490
|
+
: 2;
|
|
491
|
+
const out = [`${' '.repeat(indent)}output:`, ' '.repeat(indent + step) + 'schema' + src.lines[from].slice(src.col(keyEnd))];
|
|
492
|
+
out.push(...reindent(src.lines.slice(from + 1, to + 1), step));
|
|
493
|
+
if (plan.mime) {
|
|
494
|
+
const g = plan.gcc;
|
|
495
|
+
out.push(...reindent(renamed(src, plan.mime, 'mime'), indent + step - g.childIndent));
|
|
496
|
+
}
|
|
497
|
+
return { from, to, lines: out };
|
|
498
|
+
}
|
|
499
|
+
/** What the plan should read back as, applied to the agent's data. */
|
|
500
|
+
function expectAgent(agent, plan) {
|
|
501
|
+
const gcc = agent.generateContentConfig;
|
|
502
|
+
if (plan.schema) {
|
|
503
|
+
agent.output = { schema: agent.outputSchema };
|
|
504
|
+
delete agent.outputSchema;
|
|
505
|
+
}
|
|
506
|
+
if (!gcc)
|
|
507
|
+
return;
|
|
508
|
+
if (plan.sampling.length) {
|
|
509
|
+
const sampling = {};
|
|
510
|
+
for (const c of plan.sampling) {
|
|
511
|
+
sampling[SAMPLING.get(c.name).key] = gcc[c.name];
|
|
512
|
+
delete gcc[c.name];
|
|
513
|
+
}
|
|
514
|
+
agent.sampling = sampling;
|
|
515
|
+
}
|
|
516
|
+
if (plan.mime) {
|
|
517
|
+
agent.output = { ...(agent.output ?? {}), mime: gcc.responseMimeType };
|
|
518
|
+
delete gcc.responseMimeType;
|
|
519
|
+
}
|
|
520
|
+
if (plan.reasoning) {
|
|
521
|
+
for (const c of plan.reasoning.remove)
|
|
522
|
+
delete gcc[c.name];
|
|
523
|
+
agent.reasoning = plan.reasoning.setting;
|
|
524
|
+
}
|
|
525
|
+
if (plan.toolChoice) {
|
|
526
|
+
delete gcc.toolConfig;
|
|
527
|
+
agent.tool_choice = plan.toolChoice.setting;
|
|
528
|
+
}
|
|
529
|
+
if (plan.dropInclude) {
|
|
530
|
+
const tc = gcc.thinkingConfig;
|
|
531
|
+
delete tc.includeThoughts;
|
|
532
|
+
if (Object.keys(tc).length === 0)
|
|
533
|
+
delete gcc.thinkingConfig;
|
|
534
|
+
}
|
|
535
|
+
if (Object.keys(gcc).length === 0)
|
|
536
|
+
delete agent.generateContentConfig;
|
|
537
|
+
}
|
|
538
|
+
/** The agents of a syndicate document: the orchestrator and each subagent, with their key paths. */
|
|
539
|
+
function agentsOf(root) {
|
|
540
|
+
const out = [];
|
|
541
|
+
const orchestrator = findPair(root, 'orchestrator')?.value;
|
|
542
|
+
if (isMap(orchestrator))
|
|
543
|
+
out.push({ map: orchestrator, path: 'orchestrator', locate: (js) => js.orchestrator });
|
|
544
|
+
const subagents = findPair(root, 'subagents')?.value;
|
|
545
|
+
if (isSeq(subagents)) {
|
|
546
|
+
subagents.items.forEach((item, i) => {
|
|
547
|
+
if (isMap(item))
|
|
548
|
+
out.push({ map: item, path: `subagents[${i}]`, locate: (js) => js.subagents[i] });
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Rewrites one YAML text's v1 agent keys into v2 (see the header). Throws on
|
|
555
|
+
* text that is not one YAML document, and on an edit that would not read
|
|
556
|
+
* back as planned (a bug, never a silent change).
|
|
557
|
+
*/
|
|
558
|
+
export function migrateYaml(text) {
|
|
559
|
+
const doc = parseDocument(text);
|
|
560
|
+
if (doc.errors.length)
|
|
561
|
+
throw new Error(`not valid YAML (${doc.errors[0].code} at line ${doc.errors[0].linePos?.[0]?.line ?? '?'})`);
|
|
562
|
+
const notes = [];
|
|
563
|
+
if (!isMap(doc.contents))
|
|
564
|
+
return { text, changed: false, notes };
|
|
565
|
+
const src = new Source(text);
|
|
566
|
+
const plans = [];
|
|
567
|
+
for (const agent of agentsOf(doc.contents)) {
|
|
568
|
+
const plan = planAgent(doc, src, agent.map, agent.path, notes);
|
|
569
|
+
if (plan)
|
|
570
|
+
plans.push({ plan, locate: agent.locate });
|
|
571
|
+
}
|
|
572
|
+
if (plans.length === 0)
|
|
573
|
+
return { text, changed: false, notes };
|
|
574
|
+
const edits = plans.flatMap(({ plan }) => [renderGcc(src, plan), renderSchema(src, plan)].filter((e) => !!e));
|
|
575
|
+
edits.sort((a, b) => b.from - a.from);
|
|
576
|
+
const lines = [...src.lines];
|
|
577
|
+
for (const e of edits)
|
|
578
|
+
lines.splice(e.from, e.to - e.from + 1, ...e.lines);
|
|
579
|
+
const out = lines.join(src.eol);
|
|
580
|
+
const expected = doc.toJS();
|
|
581
|
+
for (const { plan, locate } of plans)
|
|
582
|
+
expectAgent(locate(expected), plan);
|
|
583
|
+
const back = parseDocument(out);
|
|
584
|
+
if (back.errors.length || !isDeepStrictEqual(back.toJS(), expected)) {
|
|
585
|
+
throw new Error('the rewrite does not read back as planned; file left unchanged (please report this)');
|
|
586
|
+
}
|
|
587
|
+
return { text: out, changed: out !== text, notes };
|
|
588
|
+
}
|
|
589
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
590
|
+
function yamlFiles(path) {
|
|
591
|
+
if (!statSync(path).isDirectory())
|
|
592
|
+
return [path];
|
|
593
|
+
const out = [];
|
|
594
|
+
for (const entry of readdirSync(path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
595
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
596
|
+
continue;
|
|
597
|
+
const p = join(path, entry.name);
|
|
598
|
+
if (entry.isDirectory())
|
|
599
|
+
out.push(...yamlFiles(p));
|
|
600
|
+
else if (entry.isFile() && entry.name.endsWith('.yaml'))
|
|
601
|
+
out.push(p);
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
const USAGE = `usage: yaml_codemod [--check] <file|dir>...
|
|
606
|
+
Rewrites agent v1 spellings (generateContentConfig, outputSchema) into the
|
|
607
|
+
v2 keys (sampling, output, reasoning, tool_choice), in place. A directory means every
|
|
608
|
+
*.yaml below it. --check writes nothing and exits 1 if any file would change.`;
|
|
609
|
+
export function main(argv) {
|
|
610
|
+
const check = argv.includes('--check');
|
|
611
|
+
const targets = argv.filter((a) => a !== '--check');
|
|
612
|
+
if (targets.length === 0 || targets.some((a) => a === '--help' || a === '-h')) {
|
|
613
|
+
console.log(USAGE);
|
|
614
|
+
return targets.length === 0 ? 2 : 0;
|
|
615
|
+
}
|
|
616
|
+
let changed = 0;
|
|
617
|
+
let failed = 0;
|
|
618
|
+
for (const file of targets.flatMap(yamlFiles)) {
|
|
619
|
+
let result;
|
|
620
|
+
try {
|
|
621
|
+
result = migrateYaml(readFileSync(file, 'utf8'));
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
console.error(`✗ ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
625
|
+
failed++;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (result.changed) {
|
|
629
|
+
changed++;
|
|
630
|
+
if (!check)
|
|
631
|
+
writeFileSync(file, result.text);
|
|
632
|
+
console.log(`${check ? 'would change' : 'migrated'}: ${file}`);
|
|
633
|
+
}
|
|
634
|
+
for (const note of result.notes)
|
|
635
|
+
console.log(` note: ${file}: ${note}`);
|
|
636
|
+
}
|
|
637
|
+
if (check && changed)
|
|
638
|
+
console.log(`${changed} file(s) still use v1 spellings; run without --check to rewrite them.`);
|
|
639
|
+
return failed ? 1 : check && changed ? 1 : 0;
|
|
640
|
+
}
|
|
641
|
+
const invokedAsMain = (() => {
|
|
642
|
+
if (!process.argv[1])
|
|
643
|
+
return false;
|
|
644
|
+
try {
|
|
645
|
+
return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
})();
|
|
651
|
+
if (invokedAsMain)
|
|
652
|
+
process.exitCode = main(process.argv.slice(2));
|