@akagilnc/pi-workflow-roles 0.1.5475 → 0.1.5483
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/dist/acp-host/description.js +8 -5
- package/dist/acp-host/production-host.js +16 -8
- package/dist/headless-host/description.js +251 -0
- package/dist/headless-host/production-host.js +178 -10
- package/dist/host-descriptions.js +8 -3
- package/dist/migrate-book-topology.js +9 -4
- package/dist/public-cli/main.js +9 -4
- package/package.json +1 -1
- package/src/acp-host/description.ts +19 -8
- package/src/acp-host/production-host.ts +1 -0
- package/src/acp-host/role-turn-host.ts +8 -5
- package/src/headless-host/description.ts +248 -1
- package/src/headless-host/production-host.ts +1 -2
- package/src/headless-host/role-turn-host.ts +8 -8
- package/src/host-descriptions.ts +8 -3
|
@@ -12,12 +12,15 @@ export function acpStdioArgs(description, model, seat) {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* The modelId the host addresses the seat model by.
|
|
15
|
-
* "argv" hosts
|
|
16
|
-
*
|
|
17
|
-
* alias projection
|
|
15
|
+
* "argv" hosts use the bare model on the CLI flag. "set_model" hosts use the
|
|
16
|
+
* host-native catalog form in `setModelId` (hermes `provider:model` after seat
|
|
17
|
+
* provider / host-alias projection; grok bare model — never a package map).
|
|
18
18
|
*/
|
|
19
|
-
export function acpModelId(
|
|
19
|
+
export function acpModelId(description, model) {
|
|
20
20
|
if (model?.model === undefined)
|
|
21
21
|
return undefined;
|
|
22
|
-
|
|
22
|
+
if (description.modelPassing !== "set_model")
|
|
23
|
+
return model.model;
|
|
24
|
+
const form = description.setModelId ?? "provider:model";
|
|
25
|
+
return form === "bare" ? model.model : `${model.provider}:${model.model}`;
|
|
23
26
|
}
|
|
@@ -44,15 +44,19 @@ var init_host_descriptions = __esm({
|
|
|
44
44
|
"use strict";
|
|
45
45
|
DEFAULT_ROLE_TURN_HOST = "pi";
|
|
46
46
|
HOST_DESCRIPTIONS = Object.freeze({
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Operator home `~/.grok`, native session/load resume, `agent stdio`.
|
|
49
|
+
* Model arrives as ACP `session/set_model` with bare modelId (grok catalog);
|
|
50
|
+
* argv `--model` is ignored by current grok (#1146 probe).
|
|
51
|
+
*/
|
|
48
52
|
"grok-build": Object.freeze({
|
|
49
53
|
binaryFromHome: Object.freeze([".grok", "bin", "grok"]),
|
|
50
54
|
argv: Object.freeze({
|
|
51
55
|
prefix: Object.freeze(["agent"]),
|
|
52
|
-
suffix: Object.freeze(["stdio"])
|
|
53
|
-
modelFlag: "--model"
|
|
56
|
+
suffix: Object.freeze(["stdio"])
|
|
54
57
|
}),
|
|
55
|
-
modelPassing: "
|
|
58
|
+
modelPassing: "set_model",
|
|
59
|
+
setModelId: "bare",
|
|
56
60
|
sessionBindingFile: "grok-acp-session.json"
|
|
57
61
|
}),
|
|
58
62
|
/**
|
|
@@ -70,6 +74,7 @@ var init_host_descriptions = __esm({
|
|
|
70
74
|
thinkingFlag: "--reasoning"
|
|
71
75
|
}),
|
|
72
76
|
modelPassing: "set_model",
|
|
77
|
+
setModelId: "provider:model",
|
|
73
78
|
sessionBindingFile: "hermes-acp-session.json",
|
|
74
79
|
seatProfileSoul: Object.freeze({
|
|
75
80
|
flag: "-p",
|
|
@@ -24837,9 +24842,11 @@ function acpStdioArgs(description, model, seat) {
|
|
|
24837
24842
|
...suffix
|
|
24838
24843
|
];
|
|
24839
24844
|
}
|
|
24840
|
-
function acpModelId(
|
|
24845
|
+
function acpModelId(description, model) {
|
|
24841
24846
|
if (model?.model === void 0) return void 0;
|
|
24842
|
-
|
|
24847
|
+
if (description.modelPassing !== "set_model") return model.model;
|
|
24848
|
+
const form = description.setModelId ?? "provider:model";
|
|
24849
|
+
return form === "bare" ? model.model : `${model.provider}:${model.model}`;
|
|
24843
24850
|
}
|
|
24844
24851
|
|
|
24845
24852
|
// src/acp-host/role-turn-host.ts
|
|
@@ -25226,7 +25233,7 @@ function createAcpRoleTurnHost(config) {
|
|
|
25226
25233
|
});
|
|
25227
25234
|
const initializeMeta = initialized._meta;
|
|
25228
25235
|
const availableModels = Array.isArray(initializeMeta?.modelState?.availableModels) ? initializeMeta.modelState.availableModels : void 0;
|
|
25229
|
-
if (request.model !== void 0 && availableModels !== void 0 && !availableModels.some((entry) => typeof entry === "object" && entry !== null && entry.modelId === acpModelId(config
|
|
25236
|
+
if (request.model !== void 0 && availableModels !== void 0 && !availableModels.some((entry) => typeof entry === "object" && entry !== null && entry.modelId === acpModelId(config, request.model))) {
|
|
25230
25237
|
outcome = externalHostFailure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
25231
25238
|
provider: request.model.provider,
|
|
25232
25239
|
model: request.model.model
|
|
@@ -25291,7 +25298,7 @@ function createAcpRoleTurnHost(config) {
|
|
|
25291
25298
|
if (config.modelPassing === "set_model" && request.model !== void 0 && sessionId !== void 0) {
|
|
25292
25299
|
await rpc("session/set_model", {
|
|
25293
25300
|
sessionId,
|
|
25294
|
-
modelId: acpModelId(config
|
|
25301
|
+
modelId: acpModelId(config, request.model)
|
|
25295
25302
|
});
|
|
25296
25303
|
sessionId = await loadSession(sessionId);
|
|
25297
25304
|
}
|
|
@@ -25512,6 +25519,7 @@ function createProductionAcpRoleTurnHost(options) {
|
|
|
25512
25519
|
hostName,
|
|
25513
25520
|
sessionIdentity: createSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
|
|
25514
25521
|
modelPassing: description.modelPassing,
|
|
25522
|
+
...description.setModelId === void 0 ? {} : { setModelId: description.setModelId },
|
|
25515
25523
|
roleRuntimeDependencies: createRoleRuntimeDependencies(packageRoot),
|
|
25516
25524
|
async connect(request) {
|
|
25517
25525
|
const seatProfile = description.seatProfileSoul;
|
|
@@ -54,6 +54,257 @@ function codexTomlStringTable(entries) {
|
|
|
54
54
|
const parts = Object.entries(entries).map(([key, value]) => `${key}=${codexTomlString(value)}`);
|
|
55
55
|
return `{${parts.join(",")}}`;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Free-form JSON leaf for Type.Unknown under Codex strict transport.
|
|
59
|
+
* Declared contract objects still close with additionalProperties:false.
|
|
60
|
+
* For Unknown, a $defs anyOf of JSON values with nested additionalProperties
|
|
61
|
+
* as $ref is accepted by Codex and keeps non-empty object receipts expressible
|
|
62
|
+
* (native probe). Package code still does not validate or reject the receipt.
|
|
63
|
+
*/
|
|
64
|
+
const CODEX_JSON_VALUE_DEF = "codexJsonValue";
|
|
65
|
+
const CODEX_JSON_VALUE_REF = `#/$defs/${CODEX_JSON_VALUE_DEF}`;
|
|
66
|
+
const CODEX_JSON_VALUE_SCHEMA = Object.freeze({
|
|
67
|
+
anyOf: Object.freeze([
|
|
68
|
+
Object.freeze({ type: "string" }),
|
|
69
|
+
Object.freeze({ type: "number" }),
|
|
70
|
+
Object.freeze({ type: "boolean" }),
|
|
71
|
+
Object.freeze({ type: "null" }),
|
|
72
|
+
Object.freeze({ type: "array", items: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }) }),
|
|
73
|
+
Object.freeze({
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: Object.freeze({}),
|
|
76
|
+
required: Object.freeze([]),
|
|
77
|
+
additionalProperties: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }),
|
|
78
|
+
}),
|
|
79
|
+
]),
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Derive a Codex/OpenAI-strict transport schema from the package open schema.
|
|
83
|
+
* Legal open schema is untouched; this is a host-only transmission projection
|
|
84
|
+
* (#1148 / #646 / ADR 0057 / ADR 0054): every object closes, every property is
|
|
85
|
+
* required, originally-optional fields become a null union (official guidance),
|
|
86
|
+
* and originally-required declared nullability stays. Nested open-tool anyOf
|
|
87
|
+
* wrappers are flattened so every branch carries `type`, keeping descriptions.
|
|
88
|
+
* Type.Unknown / description-only leaves become the closed JSON-value $ref.
|
|
89
|
+
* Package code still does not validate or reject the receipt.
|
|
90
|
+
*/
|
|
91
|
+
export function closeJsonSchemaForCodex(schema) {
|
|
92
|
+
const closed = closeSchemaNode(schema);
|
|
93
|
+
const existingDefs = isRecord(closed.$defs)
|
|
94
|
+
? closed.$defs
|
|
95
|
+
: {};
|
|
96
|
+
return {
|
|
97
|
+
...closed,
|
|
98
|
+
$defs: {
|
|
99
|
+
...existingDefs,
|
|
100
|
+
[CODEX_JSON_VALUE_DEF]: CODEX_JSON_VALUE_SCHEMA,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Pure null leaf only — composite type|null is stripped in-leaf, not dropped whole. */
|
|
105
|
+
function isPureNullTypeSchema(value) {
|
|
106
|
+
if (!isRecord(value))
|
|
107
|
+
return false;
|
|
108
|
+
if (value.type === "null")
|
|
109
|
+
return true;
|
|
110
|
+
if (Array.isArray(value.type) && value.type.length > 0 && value.type.every((t) => t === "null")) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
/** Declared nullability on the open schema (type|null, anyOf null leaf, or pure null). */
|
|
116
|
+
function schemaDeclaresNull(schema) {
|
|
117
|
+
if (!isRecord(schema))
|
|
118
|
+
return false;
|
|
119
|
+
if (isPureNullTypeSchema(schema))
|
|
120
|
+
return true;
|
|
121
|
+
if (Array.isArray(schema.type) && schema.type.includes("null"))
|
|
122
|
+
return true;
|
|
123
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.some(schemaDeclaresNull))
|
|
124
|
+
return true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Within a leaf, remove null members from type arrays.
|
|
129
|
+
* Null is re-added when the union or property edge originally declared it.
|
|
130
|
+
*/
|
|
131
|
+
function stripNullFromLeafType(leaf) {
|
|
132
|
+
if (!isRecord(leaf) || !Array.isArray(leaf.type))
|
|
133
|
+
return leaf;
|
|
134
|
+
const nonNull = leaf.type.filter((t) => t !== "null");
|
|
135
|
+
if (nonNull.length === leaf.type.length)
|
|
136
|
+
return leaf;
|
|
137
|
+
if (nonNull.length === 0)
|
|
138
|
+
return { ...leaf, type: "null" };
|
|
139
|
+
if (nonNull.length === 1)
|
|
140
|
+
return { ...leaf, type: nonNull[0] };
|
|
141
|
+
return { ...leaf, type: nonNull };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Flatten nested anyOf wrappers into concrete leaf schemas.
|
|
145
|
+
* Open-tool unions often wrap leaves in description-only anyOf shells that
|
|
146
|
+
* lack `type`; strict structured output rejects those intermediate nodes.
|
|
147
|
+
* Nested shell descriptions attach to leaves that lack their own (not onto $ref).
|
|
148
|
+
* The outermost union keeps its description on the closed node via the caller.
|
|
149
|
+
*/
|
|
150
|
+
function flattenUnionLeaves(schema, attachShellDescription = false) {
|
|
151
|
+
if (!isRecord(schema))
|
|
152
|
+
return [schema];
|
|
153
|
+
if (!Array.isArray(schema.anyOf))
|
|
154
|
+
return [schema];
|
|
155
|
+
const shellDescription = attachShellDescription && typeof schema.description === "string"
|
|
156
|
+
? schema.description
|
|
157
|
+
: undefined;
|
|
158
|
+
return schema.anyOf.flatMap((branch) => {
|
|
159
|
+
const leaves = flattenUnionLeaves(branch, true);
|
|
160
|
+
if (shellDescription === undefined)
|
|
161
|
+
return leaves;
|
|
162
|
+
return leaves.map((leaf) => {
|
|
163
|
+
if (!isRecord(leaf) || typeof leaf.$ref === "string")
|
|
164
|
+
return leaf;
|
|
165
|
+
if (typeof leaf.description === "string")
|
|
166
|
+
return leaf;
|
|
167
|
+
return { ...leaf, description: shellDescription };
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Drop pure-null leaves after in-leaf null stripping; property edge may re-add null. */
|
|
172
|
+
function nonNullLeaves(schema) {
|
|
173
|
+
return flattenUnionLeaves(schema)
|
|
174
|
+
.map(stripNullFromLeafType)
|
|
175
|
+
.filter((leaf) => !isPureNullTypeSchema(leaf));
|
|
176
|
+
}
|
|
177
|
+
function withDescription(closed, description) {
|
|
178
|
+
if (description.description === undefined)
|
|
179
|
+
return closed;
|
|
180
|
+
// Codex strict does not allow sibling keys on $ref; wrap so guidance is kept.
|
|
181
|
+
if (isRecord(closed) && typeof closed.$ref === "string") {
|
|
182
|
+
return { anyOf: [closed], ...description };
|
|
183
|
+
}
|
|
184
|
+
// Prefer a leaf's own description (nested anyOf shell) over the outer copy.
|
|
185
|
+
if (isRecord(closed) && typeof closed.description === "string")
|
|
186
|
+
return closed;
|
|
187
|
+
return isRecord(closed) ? { ...closed, ...description } : closed;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Property edge under strict transport.
|
|
191
|
+
* Optional → closed leaf(s) + null. Required with declared nullability keeps null.
|
|
192
|
+
* Required without declared null stays non-nullable.
|
|
193
|
+
*/
|
|
194
|
+
function closePropertySchema(schema, optional) {
|
|
195
|
+
const description = isRecord(schema) && typeof schema.description === "string"
|
|
196
|
+
? { description: schema.description }
|
|
197
|
+
: {};
|
|
198
|
+
const keepNull = optional || schemaDeclaresNull(schema);
|
|
199
|
+
const leaves = nonNullLeaves(schema).map(closeSchemaNode);
|
|
200
|
+
if (leaves.length === 0)
|
|
201
|
+
return { type: "null", ...description };
|
|
202
|
+
if (!keepNull) {
|
|
203
|
+
const closed = leaves.length === 1 ? leaves[0] : { anyOf: leaves };
|
|
204
|
+
return withDescription(closed, description);
|
|
205
|
+
}
|
|
206
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Strict generators require every schema node to declare `type` (or a $ref).
|
|
210
|
+
* Type.Unknown / description-only leaves → free JSON $ref (not string): array
|
|
211
|
+
* and object receipts stay expressible under --output-schema.
|
|
212
|
+
*/
|
|
213
|
+
function ensureTypedLeaf(schema) {
|
|
214
|
+
if (schema.type !== undefined)
|
|
215
|
+
return schema;
|
|
216
|
+
if (typeof schema.$ref === "string")
|
|
217
|
+
return schema;
|
|
218
|
+
if (isRecord(schema.properties) || schema.additionalProperties !== undefined) {
|
|
219
|
+
return { ...schema, type: "object" };
|
|
220
|
+
}
|
|
221
|
+
if (schema.items !== undefined) {
|
|
222
|
+
return { ...schema, type: "array" };
|
|
223
|
+
}
|
|
224
|
+
if (schema.const !== undefined) {
|
|
225
|
+
const value = schema.const;
|
|
226
|
+
if (value === null)
|
|
227
|
+
return { ...schema, type: "null" };
|
|
228
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
229
|
+
return { ...schema, type: typeof value };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
233
|
+
const sample = schema.enum.find((item) => item !== null);
|
|
234
|
+
if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
|
|
235
|
+
return { ...schema, type: typeof sample };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Free JSON via $defs. $ref must stand alone (strict rejects sibling keys).
|
|
239
|
+
return { $ref: CODEX_JSON_VALUE_REF };
|
|
240
|
+
}
|
|
241
|
+
function originalRequiredNames(node) {
|
|
242
|
+
if (!Array.isArray(node.required))
|
|
243
|
+
return new Set();
|
|
244
|
+
return new Set(node.required.filter((item) => typeof item === "string"));
|
|
245
|
+
}
|
|
246
|
+
function closeSchemaNode(node) {
|
|
247
|
+
if (!isRecord(node))
|
|
248
|
+
return node;
|
|
249
|
+
// Already a ref (free-JSON leaf or pre-existing) — do not retype.
|
|
250
|
+
if (typeof node.$ref === "string")
|
|
251
|
+
return node;
|
|
252
|
+
// Union node: flatten then close each concrete leaf (do not keep untyped shells).
|
|
253
|
+
// Declared null stays — including nested unions such as array items.
|
|
254
|
+
if (Array.isArray(node.anyOf)) {
|
|
255
|
+
const description = typeof node.description === "string" ? { description: node.description } : {};
|
|
256
|
+
const keepNull = schemaDeclaresNull(node);
|
|
257
|
+
const leaves = nonNullLeaves(node).map(closeSchemaNode);
|
|
258
|
+
if (leaves.length === 0)
|
|
259
|
+
return { type: "null", ...description };
|
|
260
|
+
if (!keepNull) {
|
|
261
|
+
if (leaves.length === 1)
|
|
262
|
+
return withDescription(leaves[0], description);
|
|
263
|
+
return { anyOf: leaves, ...description };
|
|
264
|
+
}
|
|
265
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
266
|
+
}
|
|
267
|
+
let out = { ...node };
|
|
268
|
+
if (node.items !== undefined) {
|
|
269
|
+
out.items = closeSchemaNode(node.items);
|
|
270
|
+
}
|
|
271
|
+
if (isRecord(node.$defs)) {
|
|
272
|
+
out.$defs = Object.fromEntries(Object.entries(node.$defs).map(([key, value]) => [key, closeSchemaNode(value)]));
|
|
273
|
+
}
|
|
274
|
+
const hasProperties = isRecord(node.properties);
|
|
275
|
+
const isObjectType = node.type === "object"
|
|
276
|
+
|| (Array.isArray(node.type) && node.type.includes("object"))
|
|
277
|
+
|| hasProperties
|
|
278
|
+
|| node.additionalProperties !== undefined;
|
|
279
|
+
if (hasProperties) {
|
|
280
|
+
const props = node.properties;
|
|
281
|
+
const wasRequired = originalRequiredNames(node);
|
|
282
|
+
const closedProps = {};
|
|
283
|
+
const required = [];
|
|
284
|
+
for (const [name, propSchema] of Object.entries(props)) {
|
|
285
|
+
required.push(name);
|
|
286
|
+
closedProps[name] = closePropertySchema(propSchema, !wasRequired.has(name));
|
|
287
|
+
}
|
|
288
|
+
out.properties = closedProps;
|
|
289
|
+
out.required = required;
|
|
290
|
+
out.additionalProperties = false;
|
|
291
|
+
if (out.type === undefined)
|
|
292
|
+
out.type = "object";
|
|
293
|
+
// Object nodes must not also carry residual anyOf from the open copy.
|
|
294
|
+
delete out.anyOf;
|
|
295
|
+
}
|
|
296
|
+
else if (isObjectType) {
|
|
297
|
+
out.additionalProperties = false;
|
|
298
|
+
if (!Array.isArray(out.required))
|
|
299
|
+
out.required = [];
|
|
300
|
+
if (out.type === undefined)
|
|
301
|
+
out.type = "object";
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
out = ensureTypedLeaf(out);
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
57
308
|
/**
|
|
58
309
|
* Project shared-envelope MCP rows into `codex -c mcp_servers.<name>.*` argv pairs.
|
|
59
310
|
* Dot-path + TOML values per official config-advanced; spawn argv (no shell).
|
|
@@ -44,15 +44,19 @@ var init_host_descriptions = __esm({
|
|
|
44
44
|
"use strict";
|
|
45
45
|
DEFAULT_ROLE_TURN_HOST = "pi";
|
|
46
46
|
HOST_DESCRIPTIONS = Object.freeze({
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Operator home `~/.grok`, native session/load resume, `agent stdio`.
|
|
49
|
+
* Model arrives as ACP `session/set_model` with bare modelId (grok catalog);
|
|
50
|
+
* argv `--model` is ignored by current grok (#1146 probe).
|
|
51
|
+
*/
|
|
48
52
|
"grok-build": Object.freeze({
|
|
49
53
|
binaryFromHome: Object.freeze([".grok", "bin", "grok"]),
|
|
50
54
|
argv: Object.freeze({
|
|
51
55
|
prefix: Object.freeze(["agent"]),
|
|
52
|
-
suffix: Object.freeze(["stdio"])
|
|
53
|
-
modelFlag: "--model"
|
|
56
|
+
suffix: Object.freeze(["stdio"])
|
|
54
57
|
}),
|
|
55
|
-
modelPassing: "
|
|
58
|
+
modelPassing: "set_model",
|
|
59
|
+
setModelId: "bare",
|
|
56
60
|
sessionBindingFile: "grok-acp-session.json"
|
|
57
61
|
}),
|
|
58
62
|
/**
|
|
@@ -70,6 +74,7 @@ var init_host_descriptions = __esm({
|
|
|
70
74
|
thinkingFlag: "--reasoning"
|
|
71
75
|
}),
|
|
72
76
|
modelPassing: "set_model",
|
|
77
|
+
setModelId: "provider:model",
|
|
73
78
|
sessionBindingFile: "hermes-acp-session.json",
|
|
74
79
|
seatProfileSoul: Object.freeze({
|
|
75
80
|
flag: "-p",
|
|
@@ -25034,6 +25039,169 @@ function codexTomlStringTable(entries) {
|
|
|
25034
25039
|
);
|
|
25035
25040
|
return `{${parts.join(",")}}`;
|
|
25036
25041
|
}
|
|
25042
|
+
var CODEX_JSON_VALUE_DEF = "codexJsonValue";
|
|
25043
|
+
var CODEX_JSON_VALUE_REF = `#/$defs/${CODEX_JSON_VALUE_DEF}`;
|
|
25044
|
+
var CODEX_JSON_VALUE_SCHEMA = Object.freeze({
|
|
25045
|
+
anyOf: Object.freeze([
|
|
25046
|
+
Object.freeze({ type: "string" }),
|
|
25047
|
+
Object.freeze({ type: "number" }),
|
|
25048
|
+
Object.freeze({ type: "boolean" }),
|
|
25049
|
+
Object.freeze({ type: "null" }),
|
|
25050
|
+
Object.freeze({ type: "array", items: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }) }),
|
|
25051
|
+
Object.freeze({
|
|
25052
|
+
type: "object",
|
|
25053
|
+
properties: Object.freeze({}),
|
|
25054
|
+
required: Object.freeze([]),
|
|
25055
|
+
additionalProperties: Object.freeze({ $ref: CODEX_JSON_VALUE_REF })
|
|
25056
|
+
})
|
|
25057
|
+
])
|
|
25058
|
+
});
|
|
25059
|
+
function closeJsonSchemaForCodex(schema) {
|
|
25060
|
+
const closed = closeSchemaNode(schema);
|
|
25061
|
+
const existingDefs = isRecord(closed.$defs) ? closed.$defs : {};
|
|
25062
|
+
return {
|
|
25063
|
+
...closed,
|
|
25064
|
+
$defs: {
|
|
25065
|
+
...existingDefs,
|
|
25066
|
+
[CODEX_JSON_VALUE_DEF]: CODEX_JSON_VALUE_SCHEMA
|
|
25067
|
+
}
|
|
25068
|
+
};
|
|
25069
|
+
}
|
|
25070
|
+
function isPureNullTypeSchema(value) {
|
|
25071
|
+
if (!isRecord(value)) return false;
|
|
25072
|
+
if (value.type === "null") return true;
|
|
25073
|
+
if (Array.isArray(value.type) && value.type.length > 0 && value.type.every((t) => t === "null")) {
|
|
25074
|
+
return true;
|
|
25075
|
+
}
|
|
25076
|
+
return false;
|
|
25077
|
+
}
|
|
25078
|
+
function schemaDeclaresNull(schema) {
|
|
25079
|
+
if (!isRecord(schema)) return false;
|
|
25080
|
+
if (isPureNullTypeSchema(schema)) return true;
|
|
25081
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
|
|
25082
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.some(schemaDeclaresNull)) return true;
|
|
25083
|
+
return false;
|
|
25084
|
+
}
|
|
25085
|
+
function stripNullFromLeafType(leaf) {
|
|
25086
|
+
if (!isRecord(leaf) || !Array.isArray(leaf.type)) return leaf;
|
|
25087
|
+
const nonNull = leaf.type.filter((t) => t !== "null");
|
|
25088
|
+
if (nonNull.length === leaf.type.length) return leaf;
|
|
25089
|
+
if (nonNull.length === 0) return { ...leaf, type: "null" };
|
|
25090
|
+
if (nonNull.length === 1) return { ...leaf, type: nonNull[0] };
|
|
25091
|
+
return { ...leaf, type: nonNull };
|
|
25092
|
+
}
|
|
25093
|
+
function flattenUnionLeaves(schema, attachShellDescription = false) {
|
|
25094
|
+
if (!isRecord(schema)) return [schema];
|
|
25095
|
+
if (!Array.isArray(schema.anyOf)) return [schema];
|
|
25096
|
+
const shellDescription = attachShellDescription && typeof schema.description === "string" ? schema.description : void 0;
|
|
25097
|
+
return schema.anyOf.flatMap((branch) => {
|
|
25098
|
+
const leaves = flattenUnionLeaves(branch, true);
|
|
25099
|
+
if (shellDescription === void 0) return leaves;
|
|
25100
|
+
return leaves.map((leaf) => {
|
|
25101
|
+
if (!isRecord(leaf) || typeof leaf.$ref === "string") return leaf;
|
|
25102
|
+
if (typeof leaf.description === "string") return leaf;
|
|
25103
|
+
return { ...leaf, description: shellDescription };
|
|
25104
|
+
});
|
|
25105
|
+
});
|
|
25106
|
+
}
|
|
25107
|
+
function nonNullLeaves(schema) {
|
|
25108
|
+
return flattenUnionLeaves(schema).map(stripNullFromLeafType).filter((leaf) => !isPureNullTypeSchema(leaf));
|
|
25109
|
+
}
|
|
25110
|
+
function withDescription(closed, description) {
|
|
25111
|
+
if (description.description === void 0) return closed;
|
|
25112
|
+
if (isRecord(closed) && typeof closed.$ref === "string") {
|
|
25113
|
+
return { anyOf: [closed], ...description };
|
|
25114
|
+
}
|
|
25115
|
+
if (isRecord(closed) && typeof closed.description === "string") return closed;
|
|
25116
|
+
return isRecord(closed) ? { ...closed, ...description } : closed;
|
|
25117
|
+
}
|
|
25118
|
+
function closePropertySchema(schema, optional) {
|
|
25119
|
+
const description = isRecord(schema) && typeof schema.description === "string" ? { description: schema.description } : {};
|
|
25120
|
+
const keepNull = optional || schemaDeclaresNull(schema);
|
|
25121
|
+
const leaves = nonNullLeaves(schema).map(closeSchemaNode);
|
|
25122
|
+
if (leaves.length === 0) return { type: "null", ...description };
|
|
25123
|
+
if (!keepNull) {
|
|
25124
|
+
const closed = leaves.length === 1 ? leaves[0] : { anyOf: leaves };
|
|
25125
|
+
return withDescription(closed, description);
|
|
25126
|
+
}
|
|
25127
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
25128
|
+
}
|
|
25129
|
+
function ensureTypedLeaf(schema) {
|
|
25130
|
+
if (schema.type !== void 0) return schema;
|
|
25131
|
+
if (typeof schema.$ref === "string") return schema;
|
|
25132
|
+
if (isRecord(schema.properties) || schema.additionalProperties !== void 0) {
|
|
25133
|
+
return { ...schema, type: "object" };
|
|
25134
|
+
}
|
|
25135
|
+
if (schema.items !== void 0) {
|
|
25136
|
+
return { ...schema, type: "array" };
|
|
25137
|
+
}
|
|
25138
|
+
if (schema.const !== void 0) {
|
|
25139
|
+
const value = schema.const;
|
|
25140
|
+
if (value === null) return { ...schema, type: "null" };
|
|
25141
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
25142
|
+
return { ...schema, type: typeof value };
|
|
25143
|
+
}
|
|
25144
|
+
}
|
|
25145
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
25146
|
+
const sample = schema.enum.find((item) => item !== null);
|
|
25147
|
+
if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
|
|
25148
|
+
return { ...schema, type: typeof sample };
|
|
25149
|
+
}
|
|
25150
|
+
}
|
|
25151
|
+
return { $ref: CODEX_JSON_VALUE_REF };
|
|
25152
|
+
}
|
|
25153
|
+
function originalRequiredNames(node) {
|
|
25154
|
+
if (!Array.isArray(node.required)) return /* @__PURE__ */ new Set();
|
|
25155
|
+
return new Set(node.required.filter((item) => typeof item === "string"));
|
|
25156
|
+
}
|
|
25157
|
+
function closeSchemaNode(node) {
|
|
25158
|
+
if (!isRecord(node)) return node;
|
|
25159
|
+
if (typeof node.$ref === "string") return node;
|
|
25160
|
+
if (Array.isArray(node.anyOf)) {
|
|
25161
|
+
const description = typeof node.description === "string" ? { description: node.description } : {};
|
|
25162
|
+
const keepNull = schemaDeclaresNull(node);
|
|
25163
|
+
const leaves = nonNullLeaves(node).map(closeSchemaNode);
|
|
25164
|
+
if (leaves.length === 0) return { type: "null", ...description };
|
|
25165
|
+
if (!keepNull) {
|
|
25166
|
+
if (leaves.length === 1) return withDescription(leaves[0], description);
|
|
25167
|
+
return { anyOf: leaves, ...description };
|
|
25168
|
+
}
|
|
25169
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
25170
|
+
}
|
|
25171
|
+
let out = { ...node };
|
|
25172
|
+
if (node.items !== void 0) {
|
|
25173
|
+
out.items = closeSchemaNode(node.items);
|
|
25174
|
+
}
|
|
25175
|
+
if (isRecord(node.$defs)) {
|
|
25176
|
+
out.$defs = Object.fromEntries(
|
|
25177
|
+
Object.entries(node.$defs).map(([key, value]) => [key, closeSchemaNode(value)])
|
|
25178
|
+
);
|
|
25179
|
+
}
|
|
25180
|
+
const hasProperties = isRecord(node.properties);
|
|
25181
|
+
const isObjectType = node.type === "object" || Array.isArray(node.type) && node.type.includes("object") || hasProperties || node.additionalProperties !== void 0;
|
|
25182
|
+
if (hasProperties) {
|
|
25183
|
+
const props = node.properties;
|
|
25184
|
+
const wasRequired = originalRequiredNames(node);
|
|
25185
|
+
const closedProps = {};
|
|
25186
|
+
const required = [];
|
|
25187
|
+
for (const [name, propSchema] of Object.entries(props)) {
|
|
25188
|
+
required.push(name);
|
|
25189
|
+
closedProps[name] = closePropertySchema(propSchema, !wasRequired.has(name));
|
|
25190
|
+
}
|
|
25191
|
+
out.properties = closedProps;
|
|
25192
|
+
out.required = required;
|
|
25193
|
+
out.additionalProperties = false;
|
|
25194
|
+
if (out.type === void 0) out.type = "object";
|
|
25195
|
+
delete out.anyOf;
|
|
25196
|
+
} else if (isObjectType) {
|
|
25197
|
+
out.additionalProperties = false;
|
|
25198
|
+
if (!Array.isArray(out.required)) out.required = [];
|
|
25199
|
+
if (out.type === void 0) out.type = "object";
|
|
25200
|
+
} else {
|
|
25201
|
+
out = ensureTypedLeaf(out);
|
|
25202
|
+
}
|
|
25203
|
+
return out;
|
|
25204
|
+
}
|
|
25037
25205
|
function stringEnvironment(value) {
|
|
25038
25206
|
const entries = Array.isArray(value) ? value.flatMap((item) => {
|
|
25039
25207
|
if (!isRecord(item) || typeof item.name !== "string" || typeof item.value !== "string") return [];
|
|
@@ -25415,12 +25583,12 @@ function createHeadlessRoleTurnHost(config) {
|
|
|
25415
25583
|
if (codex) {
|
|
25416
25584
|
if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
25417
25585
|
outputSchemaPath = join44(request.runDirectory, "headless-output-schema.json");
|
|
25418
|
-
await writeFile9(
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25586
|
+
await writeFile9(
|
|
25587
|
+
outputSchemaPath,
|
|
25588
|
+
`${JSON.stringify(closeJsonSchemaForCodex(prepared.jsonSchema), null, 2)}
|
|
25589
|
+
`,
|
|
25590
|
+
"utf8"
|
|
25591
|
+
);
|
|
25424
25592
|
}
|
|
25425
25593
|
} else {
|
|
25426
25594
|
mcpConfigPath = join44(request.runDirectory, "headless-mcp-config.json");
|
|
@@ -13,15 +13,19 @@ export function resolveHostBinary(description, operatorHome) {
|
|
|
13
13
|
}
|
|
14
14
|
export const DEFAULT_ROLE_TURN_HOST = "pi";
|
|
15
15
|
export const HOST_DESCRIPTIONS = Object.freeze({
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Operator home `~/.grok`, native session/load resume, `agent stdio`.
|
|
18
|
+
* Model arrives as ACP `session/set_model` with bare modelId (grok catalog);
|
|
19
|
+
* argv `--model` is ignored by current grok (#1146 probe).
|
|
20
|
+
*/
|
|
17
21
|
"grok-build": Object.freeze({
|
|
18
22
|
binaryFromHome: Object.freeze([".grok", "bin", "grok"]),
|
|
19
23
|
argv: Object.freeze({
|
|
20
24
|
prefix: Object.freeze(["agent"]),
|
|
21
25
|
suffix: Object.freeze(["stdio"]),
|
|
22
|
-
modelFlag: "--model",
|
|
23
26
|
}),
|
|
24
|
-
modelPassing: "
|
|
27
|
+
modelPassing: "set_model",
|
|
28
|
+
setModelId: "bare",
|
|
25
29
|
sessionBindingFile: "grok-acp-session.json",
|
|
26
30
|
}),
|
|
27
31
|
/**
|
|
@@ -39,6 +43,7 @@ export const HOST_DESCRIPTIONS = Object.freeze({
|
|
|
39
43
|
thinkingFlag: "--reasoning",
|
|
40
44
|
}),
|
|
41
45
|
modelPassing: "set_model",
|
|
46
|
+
setModelId: "provider:model",
|
|
42
47
|
sessionBindingFile: "hermes-acp-session.json",
|
|
43
48
|
seatProfileSoul: Object.freeze({
|
|
44
49
|
flag: "-p",
|
|
@@ -14736,15 +14736,19 @@ var init_host_descriptions = __esm({
|
|
|
14736
14736
|
"src/host-descriptions.ts"() {
|
|
14737
14737
|
"use strict";
|
|
14738
14738
|
HOST_DESCRIPTIONS = Object.freeze({
|
|
14739
|
-
/**
|
|
14739
|
+
/**
|
|
14740
|
+
* Operator home `~/.grok`, native session/load resume, `agent stdio`.
|
|
14741
|
+
* Model arrives as ACP `session/set_model` with bare modelId (grok catalog);
|
|
14742
|
+
* argv `--model` is ignored by current grok (#1146 probe).
|
|
14743
|
+
*/
|
|
14740
14744
|
"grok-build": Object.freeze({
|
|
14741
14745
|
binaryFromHome: Object.freeze([".grok", "bin", "grok"]),
|
|
14742
14746
|
argv: Object.freeze({
|
|
14743
14747
|
prefix: Object.freeze(["agent"]),
|
|
14744
|
-
suffix: Object.freeze(["stdio"])
|
|
14745
|
-
modelFlag: "--model"
|
|
14748
|
+
suffix: Object.freeze(["stdio"])
|
|
14746
14749
|
}),
|
|
14747
|
-
modelPassing: "
|
|
14750
|
+
modelPassing: "set_model",
|
|
14751
|
+
setModelId: "bare",
|
|
14748
14752
|
sessionBindingFile: "grok-acp-session.json"
|
|
14749
14753
|
}),
|
|
14750
14754
|
/**
|
|
@@ -14762,6 +14766,7 @@ var init_host_descriptions = __esm({
|
|
|
14762
14766
|
thinkingFlag: "--reasoning"
|
|
14763
14767
|
}),
|
|
14764
14768
|
modelPassing: "set_model",
|
|
14769
|
+
setModelId: "provider:model",
|
|
14765
14770
|
sessionBindingFile: "hermes-acp-session.json",
|
|
14766
14771
|
seatProfileSoul: Object.freeze({
|
|
14767
14772
|
flag: "-p",
|
package/dist/public-cli/main.js
CHANGED
|
@@ -15805,15 +15805,19 @@ var init_host_descriptions = __esm({
|
|
|
15805
15805
|
"use strict";
|
|
15806
15806
|
DEFAULT_ROLE_TURN_HOST = "pi";
|
|
15807
15807
|
HOST_DESCRIPTIONS = Object.freeze({
|
|
15808
|
-
/**
|
|
15808
|
+
/**
|
|
15809
|
+
* Operator home `~/.grok`, native session/load resume, `agent stdio`.
|
|
15810
|
+
* Model arrives as ACP `session/set_model` with bare modelId (grok catalog);
|
|
15811
|
+
* argv `--model` is ignored by current grok (#1146 probe).
|
|
15812
|
+
*/
|
|
15809
15813
|
"grok-build": Object.freeze({
|
|
15810
15814
|
binaryFromHome: Object.freeze([".grok", "bin", "grok"]),
|
|
15811
15815
|
argv: Object.freeze({
|
|
15812
15816
|
prefix: Object.freeze(["agent"]),
|
|
15813
|
-
suffix: Object.freeze(["stdio"])
|
|
15814
|
-
modelFlag: "--model"
|
|
15817
|
+
suffix: Object.freeze(["stdio"])
|
|
15815
15818
|
}),
|
|
15816
|
-
modelPassing: "
|
|
15819
|
+
modelPassing: "set_model",
|
|
15820
|
+
setModelId: "bare",
|
|
15817
15821
|
sessionBindingFile: "grok-acp-session.json"
|
|
15818
15822
|
}),
|
|
15819
15823
|
/**
|
|
@@ -15831,6 +15835,7 @@ var init_host_descriptions = __esm({
|
|
|
15831
15835
|
thinkingFlag: "--reasoning"
|
|
15832
15836
|
}),
|
|
15833
15837
|
modelPassing: "set_model",
|
|
15838
|
+
setModelId: "provider:model",
|
|
15834
15839
|
sessionBindingFile: "hermes-acp-session.json",
|
|
15835
15840
|
seatProfileSoul: Object.freeze({
|
|
15836
15841
|
flag: "-p",
|
package/package.json
CHANGED
|
@@ -18,11 +18,17 @@ export type AcpHostDescription = HostIdentityDescription & Readonly<{
|
|
|
18
18
|
}>;
|
|
19
19
|
/**
|
|
20
20
|
* How the seat model reaches the agent:
|
|
21
|
-
* - "argv": passed as the CLI `--model` flag
|
|
22
|
-
* - "set_model": sent as an ACP `session/set_model` RPC
|
|
23
|
-
* `
|
|
21
|
+
* - "argv": passed as the CLI `--model` flag;
|
|
22
|
+
* - "set_model": sent as an ACP `session/set_model` RPC after session new/load
|
|
23
|
+
* and before prompt. Catalog modelId shape is `setModelId` (host-native).
|
|
24
24
|
*/
|
|
25
25
|
modelPassing: "argv" | "set_model";
|
|
26
|
+
/**
|
|
27
|
+
* Host-native `session/set_model` modelId form when `modelPassing` is
|
|
28
|
+
* `"set_model"`. hermes catalogs are `provider:model`; grok catalogs are bare
|
|
29
|
+
* model ids. Ignored for `"argv"` hosts.
|
|
30
|
+
*/
|
|
31
|
+
setModelId?: "bare" | "provider:model";
|
|
26
32
|
/**
|
|
27
33
|
* When set, the production factory ensures a seat profile whose SOUL.md is a
|
|
28
34
|
* symlink to the packaged role soul, and prefixes argv with `flag <name>`.
|
|
@@ -51,14 +57,19 @@ export function acpStdioArgs(
|
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
59
|
* The modelId the host addresses the seat model by.
|
|
54
|
-
* "argv" hosts
|
|
55
|
-
*
|
|
56
|
-
* alias projection
|
|
60
|
+
* "argv" hosts use the bare model on the CLI flag. "set_model" hosts use the
|
|
61
|
+
* host-native catalog form in `setModelId` (hermes `provider:model` after seat
|
|
62
|
+
* provider / host-alias projection; grok bare model — never a package map).
|
|
57
63
|
*/
|
|
58
64
|
export function acpModelId(
|
|
59
|
-
|
|
65
|
+
description: {
|
|
66
|
+
readonly modelPassing: AcpHostDescription["modelPassing"];
|
|
67
|
+
readonly setModelId?: AcpHostDescription["setModelId"] | undefined;
|
|
68
|
+
},
|
|
60
69
|
model?: { readonly model?: string; readonly provider?: string },
|
|
61
70
|
): string | undefined {
|
|
62
71
|
if (model?.model === undefined) return undefined;
|
|
63
|
-
|
|
72
|
+
if (description.modelPassing !== "set_model") return model.model;
|
|
73
|
+
const form = description.setModelId ?? "provider:model";
|
|
74
|
+
return form === "bare" ? model.model : `${model.provider}:${model.model}`;
|
|
64
75
|
}
|
|
@@ -63,6 +63,7 @@ export function createProductionAcpRoleTurnHost(options: ProductionAcpHostOption
|
|
|
63
63
|
hostName,
|
|
64
64
|
sessionIdentity: createSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
|
|
65
65
|
modelPassing: description.modelPassing,
|
|
66
|
+
...(description.setModelId === undefined ? {} : { setModelId: description.setModelId }),
|
|
66
67
|
roleRuntimeDependencies: createRoleRuntimeDependencies(packageRoot),
|
|
67
68
|
async connect(request) {
|
|
68
69
|
const seatProfile = description.seatProfileSoul;
|
|
@@ -98,10 +98,13 @@ export type AcpRoleTurnHostConfig = Readonly<{
|
|
|
98
98
|
hostName: string;
|
|
99
99
|
/**
|
|
100
100
|
* How the seat model reaches the agent: "set_model" sends an ACP
|
|
101
|
-
* `session/set_model` RPC
|
|
102
|
-
*
|
|
101
|
+
* `session/set_model` RPC once the session exists (new or loaded); "argv"
|
|
102
|
+
* leaves it to the connect argv (--model). Catalog modelId shape is
|
|
103
|
+
* `setModelId` (host-native).
|
|
103
104
|
*/
|
|
104
105
|
modelPassing: AcpHostDescription["modelPassing"];
|
|
106
|
+
/** Host-native set_model catalog id; see AcpHostDescription.setModelId. */
|
|
107
|
+
setModelId?: AcpHostDescription["setModelId"];
|
|
105
108
|
connect(request: RoleTurnRequest): Promise<AcpConnection>;
|
|
106
109
|
prepare(request: RoleTurnRequest): Promise<PreparedRoleTurn>;
|
|
107
110
|
}>;
|
|
@@ -296,7 +299,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
296
299
|
: undefined;
|
|
297
300
|
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
298
301
|
typeof entry === "object" && entry !== null
|
|
299
|
-
&& (entry as { modelId?: unknown }).modelId === acpModelId(config
|
|
302
|
+
&& (entry as { modelId?: unknown }).modelId === acpModelId(config, request.model))) {
|
|
300
303
|
outcome = failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
301
304
|
provider: request.model.provider,
|
|
302
305
|
model: request.model.model,
|
|
@@ -354,11 +357,11 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
354
357
|
}
|
|
355
358
|
|
|
356
359
|
if (sessionReady) {
|
|
357
|
-
// set_model
|
|
360
|
+
// set_model host-native catalog id (#778 / #1146); may rebuild agent — re-bind via loadSession.
|
|
358
361
|
if (config.modelPassing === "set_model" && request.model !== undefined && sessionId !== undefined) {
|
|
359
362
|
await rpc("session/set_model", {
|
|
360
363
|
sessionId,
|
|
361
|
-
modelId: acpModelId(config
|
|
364
|
+
modelId: acpModelId(config, request.model),
|
|
362
365
|
});
|
|
363
366
|
sessionId = await loadSession(sessionId);
|
|
364
367
|
}
|
|
@@ -130,6 +130,253 @@ function codexTomlStringTable(entries: Readonly<Record<string, string>>): string
|
|
|
130
130
|
return `{${parts.join(",")}}`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Free-form JSON leaf for Type.Unknown under Codex strict transport.
|
|
135
|
+
* Declared contract objects still close with additionalProperties:false.
|
|
136
|
+
* For Unknown, a $defs anyOf of JSON values with nested additionalProperties
|
|
137
|
+
* as $ref is accepted by Codex and keeps non-empty object receipts expressible
|
|
138
|
+
* (native probe). Package code still does not validate or reject the receipt.
|
|
139
|
+
*/
|
|
140
|
+
const CODEX_JSON_VALUE_DEF = "codexJsonValue";
|
|
141
|
+
const CODEX_JSON_VALUE_REF = `#/$defs/${CODEX_JSON_VALUE_DEF}`;
|
|
142
|
+
const CODEX_JSON_VALUE_SCHEMA = Object.freeze({
|
|
143
|
+
anyOf: Object.freeze([
|
|
144
|
+
Object.freeze({ type: "string" }),
|
|
145
|
+
Object.freeze({ type: "number" }),
|
|
146
|
+
Object.freeze({ type: "boolean" }),
|
|
147
|
+
Object.freeze({ type: "null" }),
|
|
148
|
+
Object.freeze({ type: "array", items: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }) }),
|
|
149
|
+
Object.freeze({
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: Object.freeze({}),
|
|
152
|
+
required: Object.freeze([] as string[]),
|
|
153
|
+
additionalProperties: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }),
|
|
154
|
+
}),
|
|
155
|
+
]),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Derive a Codex/OpenAI-strict transport schema from the package open schema.
|
|
160
|
+
* Legal open schema is untouched; this is a host-only transmission projection
|
|
161
|
+
* (#1148 / #646 / ADR 0057 / ADR 0054): every object closes, every property is
|
|
162
|
+
* required, originally-optional fields become a null union (official guidance),
|
|
163
|
+
* and originally-required declared nullability stays. Nested open-tool anyOf
|
|
164
|
+
* wrappers are flattened so every branch carries `type`, keeping descriptions.
|
|
165
|
+
* Type.Unknown / description-only leaves become the closed JSON-value $ref.
|
|
166
|
+
* Package code still does not validate or reject the receipt.
|
|
167
|
+
*/
|
|
168
|
+
export function closeJsonSchemaForCodex(
|
|
169
|
+
schema: Readonly<Record<string, unknown>>,
|
|
170
|
+
): Record<string, unknown> {
|
|
171
|
+
const closed = closeSchemaNode(schema) as Record<string, unknown>;
|
|
172
|
+
const existingDefs = isRecord(closed.$defs)
|
|
173
|
+
? (closed.$defs as Record<string, unknown>)
|
|
174
|
+
: {};
|
|
175
|
+
return {
|
|
176
|
+
...closed,
|
|
177
|
+
$defs: {
|
|
178
|
+
...existingDefs,
|
|
179
|
+
[CODEX_JSON_VALUE_DEF]: CODEX_JSON_VALUE_SCHEMA,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Pure null leaf only — composite type|null is stripped in-leaf, not dropped whole. */
|
|
185
|
+
function isPureNullTypeSchema(value: unknown): boolean {
|
|
186
|
+
if (!isRecord(value)) return false;
|
|
187
|
+
if (value.type === "null") return true;
|
|
188
|
+
if (Array.isArray(value.type) && value.type.length > 0 && value.type.every((t) => t === "null")) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Declared nullability on the open schema (type|null, anyOf null leaf, or pure null). */
|
|
195
|
+
function schemaDeclaresNull(schema: unknown): boolean {
|
|
196
|
+
if (!isRecord(schema)) return false;
|
|
197
|
+
if (isPureNullTypeSchema(schema)) return true;
|
|
198
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
|
|
199
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.some(schemaDeclaresNull)) return true;
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Within a leaf, remove null members from type arrays.
|
|
205
|
+
* Null is re-added when the union or property edge originally declared it.
|
|
206
|
+
*/
|
|
207
|
+
function stripNullFromLeafType(leaf: unknown): unknown {
|
|
208
|
+
if (!isRecord(leaf) || !Array.isArray(leaf.type)) return leaf;
|
|
209
|
+
const nonNull = leaf.type.filter((t) => t !== "null");
|
|
210
|
+
if (nonNull.length === leaf.type.length) return leaf;
|
|
211
|
+
if (nonNull.length === 0) return { ...leaf, type: "null" };
|
|
212
|
+
if (nonNull.length === 1) return { ...leaf, type: nonNull[0] };
|
|
213
|
+
return { ...leaf, type: nonNull };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Flatten nested anyOf wrappers into concrete leaf schemas.
|
|
218
|
+
* Open-tool unions often wrap leaves in description-only anyOf shells that
|
|
219
|
+
* lack `type`; strict structured output rejects those intermediate nodes.
|
|
220
|
+
* Nested shell descriptions attach to leaves that lack their own (not onto $ref).
|
|
221
|
+
* The outermost union keeps its description on the closed node via the caller.
|
|
222
|
+
*/
|
|
223
|
+
function flattenUnionLeaves(schema: unknown, attachShellDescription = false): unknown[] {
|
|
224
|
+
if (!isRecord(schema)) return [schema];
|
|
225
|
+
if (!Array.isArray(schema.anyOf)) return [schema];
|
|
226
|
+
const shellDescription =
|
|
227
|
+
attachShellDescription && typeof schema.description === "string"
|
|
228
|
+
? schema.description
|
|
229
|
+
: undefined;
|
|
230
|
+
return schema.anyOf.flatMap((branch) => {
|
|
231
|
+
const leaves = flattenUnionLeaves(branch, true);
|
|
232
|
+
if (shellDescription === undefined) return leaves;
|
|
233
|
+
return leaves.map((leaf) => {
|
|
234
|
+
if (!isRecord(leaf) || typeof leaf.$ref === "string") return leaf;
|
|
235
|
+
if (typeof leaf.description === "string") return leaf;
|
|
236
|
+
return { ...leaf, description: shellDescription };
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Drop pure-null leaves after in-leaf null stripping; property edge may re-add null. */
|
|
242
|
+
function nonNullLeaves(schema: unknown): unknown[] {
|
|
243
|
+
return flattenUnionLeaves(schema)
|
|
244
|
+
.map(stripNullFromLeafType)
|
|
245
|
+
.filter((leaf) => !isPureNullTypeSchema(leaf));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function withDescription(closed: unknown, description: Readonly<{ description?: string }>): unknown {
|
|
249
|
+
if (description.description === undefined) return closed;
|
|
250
|
+
// Codex strict does not allow sibling keys on $ref; wrap so guidance is kept.
|
|
251
|
+
if (isRecord(closed) && typeof closed.$ref === "string") {
|
|
252
|
+
return { anyOf: [closed], ...description };
|
|
253
|
+
}
|
|
254
|
+
// Prefer a leaf's own description (nested anyOf shell) over the outer copy.
|
|
255
|
+
if (isRecord(closed) && typeof closed.description === "string") return closed;
|
|
256
|
+
return isRecord(closed) ? { ...closed, ...description } : closed;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Property edge under strict transport.
|
|
261
|
+
* Optional → closed leaf(s) + null. Required with declared nullability keeps null.
|
|
262
|
+
* Required without declared null stays non-nullable.
|
|
263
|
+
*/
|
|
264
|
+
function closePropertySchema(schema: unknown, optional: boolean): unknown {
|
|
265
|
+
const description =
|
|
266
|
+
isRecord(schema) && typeof schema.description === "string"
|
|
267
|
+
? { description: schema.description }
|
|
268
|
+
: {};
|
|
269
|
+
const keepNull = optional || schemaDeclaresNull(schema);
|
|
270
|
+
const leaves = nonNullLeaves(schema).map(closeSchemaNode);
|
|
271
|
+
if (leaves.length === 0) return { type: "null", ...description };
|
|
272
|
+
if (!keepNull) {
|
|
273
|
+
const closed = leaves.length === 1 ? leaves[0] : { anyOf: leaves };
|
|
274
|
+
return withDescription(closed, description);
|
|
275
|
+
}
|
|
276
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Strict generators require every schema node to declare `type` (or a $ref).
|
|
281
|
+
* Type.Unknown / description-only leaves → free JSON $ref (not string): array
|
|
282
|
+
* and object receipts stay expressible under --output-schema.
|
|
283
|
+
*/
|
|
284
|
+
function ensureTypedLeaf(schema: Record<string, unknown>): Record<string, unknown> {
|
|
285
|
+
if (schema.type !== undefined) return schema;
|
|
286
|
+
if (typeof schema.$ref === "string") return schema;
|
|
287
|
+
if (isRecord(schema.properties) || schema.additionalProperties !== undefined) {
|
|
288
|
+
return { ...schema, type: "object" };
|
|
289
|
+
}
|
|
290
|
+
if (schema.items !== undefined) {
|
|
291
|
+
return { ...schema, type: "array" };
|
|
292
|
+
}
|
|
293
|
+
if (schema.const !== undefined) {
|
|
294
|
+
const value = schema.const;
|
|
295
|
+
if (value === null) return { ...schema, type: "null" };
|
|
296
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
297
|
+
return { ...schema, type: typeof value };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
301
|
+
const sample = schema.enum.find((item) => item !== null);
|
|
302
|
+
if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
|
|
303
|
+
return { ...schema, type: typeof sample };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Free JSON via $defs. $ref must stand alone (strict rejects sibling keys).
|
|
307
|
+
return { $ref: CODEX_JSON_VALUE_REF };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function originalRequiredNames(node: Record<string, unknown>): ReadonlySet<string> {
|
|
311
|
+
if (!Array.isArray(node.required)) return new Set();
|
|
312
|
+
return new Set(node.required.filter((item): item is string => typeof item === "string"));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function closeSchemaNode(node: unknown): unknown {
|
|
316
|
+
if (!isRecord(node)) return node;
|
|
317
|
+
|
|
318
|
+
// Already a ref (free-JSON leaf or pre-existing) — do not retype.
|
|
319
|
+
if (typeof node.$ref === "string") return node;
|
|
320
|
+
|
|
321
|
+
// Union node: flatten then close each concrete leaf (do not keep untyped shells).
|
|
322
|
+
// Declared null stays — including nested unions such as array items.
|
|
323
|
+
if (Array.isArray(node.anyOf)) {
|
|
324
|
+
const description =
|
|
325
|
+
typeof node.description === "string" ? { description: node.description } : {};
|
|
326
|
+
const keepNull = schemaDeclaresNull(node);
|
|
327
|
+
const leaves = nonNullLeaves(node).map(closeSchemaNode);
|
|
328
|
+
if (leaves.length === 0) return { type: "null", ...description };
|
|
329
|
+
if (!keepNull) {
|
|
330
|
+
if (leaves.length === 1) return withDescription(leaves[0], description);
|
|
331
|
+
return { anyOf: leaves, ...description };
|
|
332
|
+
}
|
|
333
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let out: Record<string, unknown> = { ...node };
|
|
337
|
+
|
|
338
|
+
if (node.items !== undefined) {
|
|
339
|
+
out.items = closeSchemaNode(node.items);
|
|
340
|
+
}
|
|
341
|
+
if (isRecord(node.$defs)) {
|
|
342
|
+
out.$defs = Object.fromEntries(
|
|
343
|
+
Object.entries(node.$defs).map(([key, value]) => [key, closeSchemaNode(value)]),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const hasProperties = isRecord(node.properties);
|
|
348
|
+
const isObjectType =
|
|
349
|
+
node.type === "object"
|
|
350
|
+
|| (Array.isArray(node.type) && node.type.includes("object"))
|
|
351
|
+
|| hasProperties
|
|
352
|
+
|| node.additionalProperties !== undefined;
|
|
353
|
+
|
|
354
|
+
if (hasProperties) {
|
|
355
|
+
const props = node.properties as Record<string, unknown>;
|
|
356
|
+
const wasRequired = originalRequiredNames(node);
|
|
357
|
+
const closedProps: Record<string, unknown> = {};
|
|
358
|
+
const required: string[] = [];
|
|
359
|
+
for (const [name, propSchema] of Object.entries(props)) {
|
|
360
|
+
required.push(name);
|
|
361
|
+
closedProps[name] = closePropertySchema(propSchema, !wasRequired.has(name));
|
|
362
|
+
}
|
|
363
|
+
out.properties = closedProps;
|
|
364
|
+
out.required = required;
|
|
365
|
+
out.additionalProperties = false;
|
|
366
|
+
if (out.type === undefined) out.type = "object";
|
|
367
|
+
// Object nodes must not also carry residual anyOf from the open copy.
|
|
368
|
+
delete out.anyOf;
|
|
369
|
+
} else if (isObjectType) {
|
|
370
|
+
out.additionalProperties = false;
|
|
371
|
+
if (!Array.isArray(out.required)) out.required = [];
|
|
372
|
+
if (out.type === undefined) out.type = "object";
|
|
373
|
+
} else {
|
|
374
|
+
out = ensureTypedLeaf(out);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
133
380
|
/**
|
|
134
381
|
* Project shared-envelope MCP rows into `codex -c mcp_servers.<name>.*` argv pairs.
|
|
135
382
|
* Dot-path + TOML values per official config-advanced; spawn argv (no shell).
|
|
@@ -185,7 +432,7 @@ export function codexTurnArgs(options: {
|
|
|
185
432
|
readonly systemPromptPath: string;
|
|
186
433
|
/**
|
|
187
434
|
* Absolute path for the native `--output-schema` flag.
|
|
188
|
-
*
|
|
435
|
+
* File holds the Codex-strict transport projection of the open declaration (#1148).
|
|
189
436
|
* Optional for prose-exit seats (#959 navigator) so agent_message stays free text.
|
|
190
437
|
*/
|
|
191
438
|
readonly outputSchemaPath?: string;
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* Intermediate AK tools ride the shared envelope MCP relay (Claude `--mcp-config`,
|
|
8
8
|
* codex `-c mcp_servers.*`). The terminating receipt is the host-native schema
|
|
9
9
|
* channel only (#750 submission-tool-is-schema-channel) — terminating tool is not
|
|
10
|
-
* listed on MCP (Claude `--json-schema`, codex `--output-schema`
|
|
11
|
-
* Codex native strict output cannot promise an open contract; the adapter reports that gap.
|
|
10
|
+
* listed on MCP (Claude `--json-schema`, codex `--output-schema` closed projection).
|
|
12
11
|
*/
|
|
13
12
|
import { randomUUID } from "node:crypto";
|
|
14
13
|
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
recordNativeSessionPointer,
|
|
33
33
|
} from "../host-session-record.ts";
|
|
34
34
|
import {
|
|
35
|
+
closeJsonSchemaForCodex,
|
|
35
36
|
codexTurnArgs,
|
|
36
37
|
headlessMcpConfigDocument,
|
|
37
38
|
headlessTurnArgs,
|
|
@@ -465,16 +466,15 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
465
466
|
let mcpConfigPath: string | undefined;
|
|
466
467
|
let outputSchemaPath: string | undefined;
|
|
467
468
|
if (codex) {
|
|
468
|
-
//
|
|
469
|
-
//
|
|
469
|
+
// #1148: Codex --output-schema requires a native strict transport schema.
|
|
470
|
+
// Project from the unique open declaration; do not alter package receipt rules.
|
|
470
471
|
if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
471
472
|
outputSchemaPath = join(request.runDirectory, "headless-output-schema.json");
|
|
472
|
-
await writeFile(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
});
|
|
473
|
+
await writeFile(
|
|
474
|
+
outputSchemaPath,
|
|
475
|
+
`${JSON.stringify(closeJsonSchemaForCodex(prepared.jsonSchema), null, 2)}\n`,
|
|
476
|
+
"utf8",
|
|
477
|
+
);
|
|
478
478
|
}
|
|
479
479
|
} else {
|
|
480
480
|
mcpConfigPath = join(request.runDirectory, "headless-mcp-config.json");
|
package/src/host-descriptions.ts
CHANGED
|
@@ -32,15 +32,19 @@ export const DEFAULT_ROLE_TURN_HOST = "pi" as const;
|
|
|
32
32
|
export type HostFamily = "acp" | "headless";
|
|
33
33
|
|
|
34
34
|
export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = Object.freeze({
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Operator home `~/.grok`, native session/load resume, `agent stdio`.
|
|
37
|
+
* Model arrives as ACP `session/set_model` with bare modelId (grok catalog);
|
|
38
|
+
* argv `--model` is ignored by current grok (#1146 probe).
|
|
39
|
+
*/
|
|
36
40
|
"grok-build": Object.freeze({
|
|
37
41
|
binaryFromHome: Object.freeze([".grok", "bin", "grok"]),
|
|
38
42
|
argv: Object.freeze({
|
|
39
43
|
prefix: Object.freeze(["agent"]),
|
|
40
44
|
suffix: Object.freeze(["stdio"]),
|
|
41
|
-
modelFlag: "--model",
|
|
42
45
|
}),
|
|
43
|
-
modelPassing: "
|
|
46
|
+
modelPassing: "set_model",
|
|
47
|
+
setModelId: "bare",
|
|
44
48
|
sessionBindingFile: "grok-acp-session.json",
|
|
45
49
|
}),
|
|
46
50
|
/**
|
|
@@ -58,6 +62,7 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
|
|
|
58
62
|
thinkingFlag: "--reasoning",
|
|
59
63
|
}),
|
|
60
64
|
modelPassing: "set_model",
|
|
65
|
+
setModelId: "provider:model",
|
|
61
66
|
sessionBindingFile: "hermes-acp-session.json",
|
|
62
67
|
seatProfileSoul: Object.freeze({
|
|
63
68
|
flag: "-p",
|