@xemahq/agent-session-runtime 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +175 -201
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/composition-resolution.d.ts +43 -0
- package/dist/lib/composition-resolution.d.ts.map +1 -0
- package/dist/lib/composition-resolution.js +236 -0
- package/dist/lib/composition-resolution.js.map +1 -0
- package/dist/lib/composition-workspace-manifest.d.ts +5 -0
- package/dist/lib/composition-workspace-manifest.d.ts.map +1 -1
- package/dist/lib/composition-workspace-manifest.js +12 -1
- package/dist/lib/composition-workspace-manifest.js.map +1 -1
- package/package.json +8 -3
- package/src/index.ts +1 -0
- package/src/lib/composition-resolution.ts +524 -0
- package/src/lib/composition-workspace-manifest.ts +32 -1
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Shared composition-resolver primitives ──
|
|
3
|
+
//
|
|
4
|
+
// The "one resolver, not N" foundation. Composition (agent) tree resolution
|
|
5
|
+
// — walk the tree, collect skills / tools, append instructions, merge the
|
|
6
|
+
// permission/model overlay, and project to a runtime config — was
|
|
7
|
+
// re-implemented in agent-session-api, the workflow-runtime-worker, and
|
|
8
|
+
// workspace-proxy with drift and two live bugs (the worker dropped sub-agent
|
|
9
|
+
// `modelOverride` and collected only the root node's skills). This module
|
|
10
|
+
// extracts the PURE parts so every consumer shares ONE implementation.
|
|
11
|
+
//
|
|
12
|
+
// All inputs are kernel `ResolvedComposition` / `ResolvedCompositionNode`
|
|
13
|
+
// shapes (`@xemahq/kernel-contracts/agent-composition`). Each node `extends
|
|
14
|
+
// NodeOverlayFragment`, so its `modelOverride` / `instructions` / `permission`
|
|
15
|
+
// overlay trio is read directly off the node — no per-shape restatement.
|
|
16
|
+
//
|
|
17
|
+
// Tier-1 functions (collect / walk) copy the canonical agent-session-api
|
|
18
|
+
// resolver semantics EXACTLY: depth-first PRE-ORDER over DESCENDANTS,
|
|
19
|
+
// FIRST-occurrence-by-slug wins. Tier-2 (`projectCompositionToRuntimeConfig`)
|
|
20
|
+
// assembles those into the runtime projection.
|
|
21
|
+
//
|
|
22
|
+
// `RuntimeAgentConfig` lives HERE, not in `@xemahq/kernel-contracts`, because
|
|
23
|
+
// it is a runtime PROJECTION (assembled by this resolver for in-process
|
|
24
|
+
// consumers) — not a wire contract crossing a service boundary. Kernel
|
|
25
|
+
// contracts are reserved for shapes two services must agree on; this is an
|
|
26
|
+
// implementation detail of the shared resolver.
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
ModelRefKind,
|
|
31
|
+
MODEL_CLASS_VALUES,
|
|
32
|
+
type ModelClass,
|
|
33
|
+
type ModelRef,
|
|
34
|
+
type NodeOverlayFragment,
|
|
35
|
+
type SubAgentBinding,
|
|
36
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
37
|
+
import type {
|
|
38
|
+
ResolvedComposition,
|
|
39
|
+
ResolvedCompositionNode,
|
|
40
|
+
} from '@xemahq/kernel-contracts/agent-composition';
|
|
41
|
+
import type { PermissionMap } from '@xemahq/kernel-contracts/agent-permission';
|
|
42
|
+
import {
|
|
43
|
+
ToolProviderKind,
|
|
44
|
+
type ToolSelectionEntry,
|
|
45
|
+
} from '@xemahq/kernel-contracts/mcp-tool';
|
|
46
|
+
|
|
47
|
+
// ─── Errors ──────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Raised when a resolved composition node carries a malformed overlay the
|
|
51
|
+
* resolver cannot project — an invalid `modelOverride` or `tools` entry. The
|
|
52
|
+
* composition resolver in llm-registry-api owns the wire format, so drift is
|
|
53
|
+
* a platform bug, not a user error. Fail-fast: no silent coercion.
|
|
54
|
+
*/
|
|
55
|
+
export class CompositionResolutionError extends Error {
|
|
56
|
+
constructor(context: string, detail: string) {
|
|
57
|
+
super(`Composition resolution failed for "${context}": ${detail}`);
|
|
58
|
+
this.name = 'CompositionResolutionError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Local types ──────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The flattened, runtime-facing form of one DESCENDANT composition node —
|
|
66
|
+
* its slug plus the {@link NodeOverlayFragment} overlay trio carried verbatim
|
|
67
|
+
* off the resolved node. THE worker bug fix: `modelOverride` is carried, not
|
|
68
|
+
* dropped. Mirrors {@link SubAgentBinding} minus the kernel's intrinsic-floor
|
|
69
|
+
* semantics (those are merged downstream, per-consumer).
|
|
70
|
+
*/
|
|
71
|
+
export interface FlattenedCompositionNode {
|
|
72
|
+
readonly slug: string;
|
|
73
|
+
readonly alias?: string;
|
|
74
|
+
readonly modelOverride?: ModelRef;
|
|
75
|
+
readonly instructions?: string;
|
|
76
|
+
readonly permission?: PermissionMap;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Scope selector for the tree-collectors. */
|
|
80
|
+
export interface CollectScopeOptions {
|
|
81
|
+
/**
|
|
82
|
+
* `'tree'` = root node ∪ every descendant (de-duped, first-seen order).
|
|
83
|
+
* `'root'` = the root node only.
|
|
84
|
+
*/
|
|
85
|
+
readonly scope?: 'tree' | 'root';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The runtime projection of a resolved composition: everything a runtime
|
|
90
|
+
* driver needs to materialize the primary agent + its delegates. A
|
|
91
|
+
* PROJECTION, not a wire contract — lives in this lib, not kernel-contracts.
|
|
92
|
+
*/
|
|
93
|
+
export interface RuntimeAgentConfig {
|
|
94
|
+
readonly compositionRef: string;
|
|
95
|
+
readonly primaryAgentSlug: string;
|
|
96
|
+
/** Root instructions (+ overlay), normalized; `null` when none. */
|
|
97
|
+
readonly primaryInstructions: string | null;
|
|
98
|
+
/** Root permission (+ overlay); `null` when none. */
|
|
99
|
+
readonly primaryPermission: PermissionMap | null;
|
|
100
|
+
/** Root model override (+ overlay); `null` when none. */
|
|
101
|
+
readonly primaryModelOverride: ModelRef | null;
|
|
102
|
+
/** Flattened descendant bindings — NOW carrying the overlay trio. */
|
|
103
|
+
readonly subAgentBindings: readonly SubAgentBinding[];
|
|
104
|
+
readonly subAgentInstructionsBySlug: Readonly<Record<string, string>>;
|
|
105
|
+
readonly subAgentPermissionBySlug: Readonly<Record<string, PermissionMap>>;
|
|
106
|
+
readonly subAgentModelOverridesBySlug: Readonly<Record<string, ModelRef>>;
|
|
107
|
+
/** Root-scoped skill slugs. */
|
|
108
|
+
readonly defaultSkillSlugs: readonly string[];
|
|
109
|
+
/** Whole-tree skill slugs (root ∪ descendants). */
|
|
110
|
+
readonly allSkillSlugs: readonly string[];
|
|
111
|
+
/** Root-scoped tool selection. */
|
|
112
|
+
readonly defaultToolSelection: readonly ToolSelectionEntry[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Tier-1: collect / walk ───────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Flatten the resolved composition tree's DESCENDANT nodes (NOT the root)
|
|
119
|
+
* into a flat list. Depth-first PRE-ORDER; on a slug appearing more than once
|
|
120
|
+
* the FIRST occurrence wins so its `alias` / overlay trio are deterministic.
|
|
121
|
+
* A node with an empty slug is skipped (defends against a malformed resolve).
|
|
122
|
+
*
|
|
123
|
+
* This is where the worker drift is corrected: `modelOverride` is carried on
|
|
124
|
+
* every entry, never dropped.
|
|
125
|
+
*/
|
|
126
|
+
export function flattenCompositionTree(
|
|
127
|
+
root: ResolvedCompositionNode,
|
|
128
|
+
): readonly FlattenedCompositionNode[] {
|
|
129
|
+
const seen = new Set<string>();
|
|
130
|
+
const out: FlattenedCompositionNode[] = [];
|
|
131
|
+
const visit = (node: ResolvedCompositionNode): void => {
|
|
132
|
+
for (const child of node.children) {
|
|
133
|
+
const slug = child.agent.slug;
|
|
134
|
+
if (slug.length === 0) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!seen.has(slug)) {
|
|
138
|
+
seen.add(slug);
|
|
139
|
+
out.push({
|
|
140
|
+
slug,
|
|
141
|
+
...(child.alias ? { alias: child.alias } : {}),
|
|
142
|
+
...(child.modelOverride
|
|
143
|
+
? { modelOverride: child.modelOverride }
|
|
144
|
+
: {}),
|
|
145
|
+
...(child.instructions ? { instructions: child.instructions } : {}),
|
|
146
|
+
...(child.permission ? { permission: child.permission } : {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
visit(child);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
visit(root);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Collect skill slugs. `scope: 'tree'` (default) = de-duped union of the root
|
|
158
|
+
* node's skills plus every descendant's, in first-seen order — fixes the
|
|
159
|
+
* worker "root-skills-only" bug. `scope: 'root'` = the root node's skills
|
|
160
|
+
* only.
|
|
161
|
+
*/
|
|
162
|
+
export function collectSkills(
|
|
163
|
+
root: ResolvedCompositionNode,
|
|
164
|
+
opts?: CollectScopeOptions,
|
|
165
|
+
): readonly string[] {
|
|
166
|
+
const scope = opts?.scope ?? 'tree';
|
|
167
|
+
const seen = new Set<string>();
|
|
168
|
+
const out: string[] = [];
|
|
169
|
+
const addNode = (node: ResolvedCompositionNode): void => {
|
|
170
|
+
for (const slug of projectSkillSlugs(node.skills)) {
|
|
171
|
+
if (!seen.has(slug)) {
|
|
172
|
+
seen.add(slug);
|
|
173
|
+
out.push(slug);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
addNode(root);
|
|
178
|
+
if (scope === 'tree') {
|
|
179
|
+
const visit = (node: ResolvedCompositionNode): void => {
|
|
180
|
+
for (const child of node.children) {
|
|
181
|
+
addNode(child);
|
|
182
|
+
visit(child);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
visit(root);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Collect tool selection. `scope: 'root'` (default — matches today's
|
|
192
|
+
* behaviour) = the root node's tools. `scope: 'tree'` = the root node's tools
|
|
193
|
+
* followed by every descendant's, in pre-order. Every entry is validated
|
|
194
|
+
* fail-fast via {@link projectToolSelection}.
|
|
195
|
+
*/
|
|
196
|
+
export function collectTools(
|
|
197
|
+
root: ResolvedCompositionNode,
|
|
198
|
+
opts?: CollectScopeOptions,
|
|
199
|
+
): readonly ToolSelectionEntry[] {
|
|
200
|
+
const scope = opts?.scope ?? 'root';
|
|
201
|
+
const ctx = root.agent.slug;
|
|
202
|
+
if (scope === 'root') {
|
|
203
|
+
return projectToolSelection(root.tools, ctx);
|
|
204
|
+
}
|
|
205
|
+
const out: ToolSelectionEntry[] = [...projectToolSelection(root.tools, ctx)];
|
|
206
|
+
const visit = (node: ResolvedCompositionNode): void => {
|
|
207
|
+
for (const child of node.children) {
|
|
208
|
+
out.push(...projectToolSelection(child.tools, child.agent.slug));
|
|
209
|
+
visit(child);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
visit(root);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Per-descendant-slug normalized instructions, keyed by agent slug. Mirrors
|
|
218
|
+
* {@link flattenCompositionTree} (DFS pre-order, first-wins). A descendant
|
|
219
|
+
* with no (or whitespace-only) instructions contributes no key.
|
|
220
|
+
*/
|
|
221
|
+
export function collectInstructionsBySlug(
|
|
222
|
+
root: ResolvedCompositionNode,
|
|
223
|
+
): Readonly<Record<string, string>> {
|
|
224
|
+
return collectDescendantOverlay(root, (child) =>
|
|
225
|
+
normalizeInstructions(child.instructions),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Per-descendant-slug permission overlay, keyed by agent slug. DFS pre-order,
|
|
231
|
+
* first-wins. A descendant with no (or empty) override contributes no key.
|
|
232
|
+
*/
|
|
233
|
+
export function collectPermissionsBySlug(
|
|
234
|
+
root: ResolvedCompositionNode,
|
|
235
|
+
): Readonly<Record<string, PermissionMap>> {
|
|
236
|
+
return collectDescendantOverlay(root, (child) =>
|
|
237
|
+
normalizePermission(child.permission),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Per-descendant-slug `modelOverride`, keyed by agent slug — the BUG-2
|
|
243
|
+
* surface (the worker dropped these). DFS pre-order, first-wins. A descendant
|
|
244
|
+
* with no override contributes no key. Each override is validated fail-fast.
|
|
245
|
+
*/
|
|
246
|
+
export function collectModelOverridesBySlug(
|
|
247
|
+
root: ResolvedCompositionNode,
|
|
248
|
+
): Readonly<Record<string, ModelRef>> {
|
|
249
|
+
return collectDescendantOverlay(root, (child) =>
|
|
250
|
+
projectModelOverride(child.modelOverride, child.agent.slug),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Shared DFS-pre-order, first-occurrence-wins walk over the DESCENDANT nodes,
|
|
256
|
+
* projecting each child to an optional overlay value via `project`. A `null`
|
|
257
|
+
* projection contributes no key — keeping the wire byte-identical for nodes
|
|
258
|
+
* with no override. Centralizes the walk the three per-slug collectors share.
|
|
259
|
+
*/
|
|
260
|
+
function collectDescendantOverlay<T>(
|
|
261
|
+
root: ResolvedCompositionNode,
|
|
262
|
+
project: (child: ResolvedCompositionNode) => T | null,
|
|
263
|
+
): Readonly<Record<string, T>> {
|
|
264
|
+
const out: Record<string, T> = {};
|
|
265
|
+
const seen = new Set<string>();
|
|
266
|
+
const visit = (node: ResolvedCompositionNode): void => {
|
|
267
|
+
for (const child of node.children) {
|
|
268
|
+
const slug = child.agent.slug;
|
|
269
|
+
if (slug.length === 0) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (!seen.has(slug)) {
|
|
273
|
+
seen.add(slug);
|
|
274
|
+
const value = project(child);
|
|
275
|
+
if (value !== null) {
|
|
276
|
+
out[slug] = value;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
visit(child);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
visit(root);
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Instruction-append primitive ─────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* THE single "append node instructions after the base prompt" primitive every
|
|
290
|
+
* consumer shares. Base prompt FIRST, a blank-line separator, then the
|
|
291
|
+
* trimmed instructions. Empty / whitespace-only / `null` instructions leave
|
|
292
|
+
* the base prompt UNCHANGED — no trailing append — so a no-instructions node
|
|
293
|
+
* is byte-identical to the pre-instructions behaviour.
|
|
294
|
+
*/
|
|
295
|
+
export function appendInstructions(
|
|
296
|
+
baseSystemPrompt: string,
|
|
297
|
+
nodeInstructions: string | null,
|
|
298
|
+
): string {
|
|
299
|
+
const trimmed = nodeInstructions === null ? '' : nodeInstructions.trim();
|
|
300
|
+
if (trimmed.length === 0) {
|
|
301
|
+
return baseSystemPrompt;
|
|
302
|
+
}
|
|
303
|
+
// Empty base (e.g. a root node with no own instructions layered with an
|
|
304
|
+
// overlay) ⇒ the instructions ARE the result, with no leading separator.
|
|
305
|
+
if (baseSystemPrompt.length === 0) {
|
|
306
|
+
return trimmed;
|
|
307
|
+
}
|
|
308
|
+
return `${baseSystemPrompt}\n\n${trimmed}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── Normalizers / projectors (copied from the canonical resolver) ────────────
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Normalize a node's optional `instructions` to a trimmed non-empty string or
|
|
315
|
+
* `null`. A node that omits instructions — or declares a whitespace-only
|
|
316
|
+
* value — yields `null` so the render path stays byte-identical to the
|
|
317
|
+
* pre-instructions behaviour. The length cap is enforced upstream at
|
|
318
|
+
* composition write time; this is not a re-validation.
|
|
319
|
+
*/
|
|
320
|
+
export function normalizeInstructions(raw: string | undefined): string | null {
|
|
321
|
+
if (typeof raw !== 'string') {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const trimmed = raw.trim();
|
|
325
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Normalize a node's optional `permission` override to a non-empty map or
|
|
330
|
+
* `null`. Structural validation happened upstream at composition write time;
|
|
331
|
+
* this only drops an absent / empty map so the wire stays byte-identical for
|
|
332
|
+
* nodes with no override.
|
|
333
|
+
*/
|
|
334
|
+
export function normalizePermission(
|
|
335
|
+
raw: PermissionMap | undefined,
|
|
336
|
+
): PermissionMap | null {
|
|
337
|
+
if (!raw || typeof raw !== 'object' || Object.keys(raw).length === 0) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return raw;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Project a node's optional `modelOverride` into a validated kernel
|
|
345
|
+
* {@link ModelRef}, or `null` when absent. Fail-fast on shape errors — the
|
|
346
|
+
* composition resolver owns the wire format, so drift is a platform bug.
|
|
347
|
+
*/
|
|
348
|
+
export function projectModelOverride(
|
|
349
|
+
raw: ModelRef | undefined,
|
|
350
|
+
context: string,
|
|
351
|
+
): ModelRef | null {
|
|
352
|
+
if (raw === undefined) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
if (raw.kind === ModelRefKind.CONCRETE) {
|
|
356
|
+
if (typeof raw.modelId !== 'string' || raw.modelId.trim().length === 0) {
|
|
357
|
+
throw new CompositionResolutionError(
|
|
358
|
+
context,
|
|
359
|
+
'concrete modelOverride is missing a modelId',
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return raw;
|
|
363
|
+
}
|
|
364
|
+
// `kind` is the closed `concrete | strategy` set — this is the strategy
|
|
365
|
+
// branch.
|
|
366
|
+
const modelClass = raw.modelClass;
|
|
367
|
+
if (
|
|
368
|
+
modelClass === undefined ||
|
|
369
|
+
!MODEL_CLASS_VALUES.includes(modelClass as ModelClass)
|
|
370
|
+
) {
|
|
371
|
+
throw new CompositionResolutionError(
|
|
372
|
+
context,
|
|
373
|
+
`strategy modelOverride.modelClass must be one of ${MODEL_CLASS_VALUES.join(
|
|
374
|
+
', ',
|
|
375
|
+
)}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return raw;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Project a node's skill refs into a flat, non-empty slug list. */
|
|
382
|
+
function projectSkillSlugs(
|
|
383
|
+
skills: ResolvedCompositionNode['skills'] | undefined,
|
|
384
|
+
): string[] {
|
|
385
|
+
if (!Array.isArray(skills)) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
return skills
|
|
389
|
+
.map((s) => s.slug)
|
|
390
|
+
.filter(
|
|
391
|
+
(slug): slug is string => typeof slug === 'string' && slug.length > 0,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Project a node's tool refs into the validated `ToolSelectionEntry[]` shape.
|
|
397
|
+
* Fail-fast on shape errors — the composition resolver owns the wire format.
|
|
398
|
+
*/
|
|
399
|
+
function projectToolSelection(
|
|
400
|
+
tools: ResolvedCompositionNode['tools'] | undefined,
|
|
401
|
+
context: string,
|
|
402
|
+
): ToolSelectionEntry[] {
|
|
403
|
+
if (!Array.isArray(tools)) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
const validKinds = new Set<string>(Object.values(ToolProviderKind));
|
|
407
|
+
const out: ToolSelectionEntry[] = [];
|
|
408
|
+
for (const [idx, tool] of tools.entries()) {
|
|
409
|
+
if (
|
|
410
|
+
typeof tool.providerKind !== 'string' ||
|
|
411
|
+
!validKinds.has(tool.providerKind)
|
|
412
|
+
) {
|
|
413
|
+
throw new CompositionResolutionError(
|
|
414
|
+
context,
|
|
415
|
+
`tools[${idx}].providerKind must be one of ${Object.values(
|
|
416
|
+
ToolProviderKind,
|
|
417
|
+
).join(', ')}`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (
|
|
421
|
+
typeof tool.resourceId !== 'string' ||
|
|
422
|
+
tool.resourceId.trim().length === 0
|
|
423
|
+
) {
|
|
424
|
+
throw new CompositionResolutionError(
|
|
425
|
+
context,
|
|
426
|
+
`tools[${idx}].resourceId must be a non-empty string`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (tool.kind === 'provider') {
|
|
430
|
+
out.push({
|
|
431
|
+
kind: 'provider',
|
|
432
|
+
providerKind: tool.providerKind,
|
|
433
|
+
resourceId: tool.resourceId,
|
|
434
|
+
});
|
|
435
|
+
} else if (tool.kind === 'tool') {
|
|
436
|
+
if (
|
|
437
|
+
typeof tool.toolName !== 'string' ||
|
|
438
|
+
tool.toolName.trim().length === 0
|
|
439
|
+
) {
|
|
440
|
+
throw new CompositionResolutionError(
|
|
441
|
+
context,
|
|
442
|
+
`tools[${idx}].toolName must be a non-empty string for kind='tool'`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
out.push({
|
|
446
|
+
kind: 'tool',
|
|
447
|
+
providerKind: tool.providerKind,
|
|
448
|
+
resourceId: tool.resourceId,
|
|
449
|
+
toolName: tool.toolName,
|
|
450
|
+
});
|
|
451
|
+
} else {
|
|
452
|
+
throw new CompositionResolutionError(
|
|
453
|
+
context,
|
|
454
|
+
`tools[${idx}].kind must be "provider" or "tool"`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── Tier-2: projection ───────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Project a `ResolvedComposition` into a {@link RuntimeAgentConfig} — the
|
|
465
|
+
* runtime-facing shape a driver materializes the primary agent + delegates
|
|
466
|
+
* from. The single resolver every consumer shares.
|
|
467
|
+
*
|
|
468
|
+
* The optional `overlay` (a session/step-level {@link NodeOverlayFragment})
|
|
469
|
+
* applies ON TOP of the root node:
|
|
470
|
+
* - `primaryInstructions` = appendInstructions(root instructions, overlay
|
|
471
|
+
* instructions) — base root prompt fragment FIRST, then the overlay.
|
|
472
|
+
* - `primaryModelOverride` = overlay.modelOverride ?? root.modelOverride —
|
|
473
|
+
* the overlay wins; absent ⇒ root.
|
|
474
|
+
* - `primaryPermission` = overlay.permission ?? root.permission — the
|
|
475
|
+
* overlay wins; absent ⇒ root. Restrict-only semantics: the overlay only
|
|
476
|
+
* narrows (it replaces the root's permission map wholesale here; per-key
|
|
477
|
+
* deep-merge against the AGENT's authored permission happens in the
|
|
478
|
+
* downstream materializer, not in this projection).
|
|
479
|
+
*/
|
|
480
|
+
export function projectCompositionToRuntimeConfig(
|
|
481
|
+
resolved: ResolvedComposition,
|
|
482
|
+
overlay?: NodeOverlayFragment,
|
|
483
|
+
): RuntimeAgentConfig {
|
|
484
|
+
const root = resolved.root;
|
|
485
|
+
const compositionRef = `${resolved.slug}@${resolved.version}`;
|
|
486
|
+
|
|
487
|
+
const rootInstructions = normalizeInstructions(root.instructions);
|
|
488
|
+
const overlayInstructions = normalizeInstructions(overlay?.instructions);
|
|
489
|
+
// Layer the overlay fragment AFTER the root's own instructions fragment.
|
|
490
|
+
const primaryInstructions = appendInstructions(
|
|
491
|
+
rootInstructions ?? '',
|
|
492
|
+
overlayInstructions,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
const rootModelOverride = projectModelOverride(
|
|
496
|
+
root.modelOverride,
|
|
497
|
+
compositionRef,
|
|
498
|
+
);
|
|
499
|
+
const overlayModelOverride = projectModelOverride(
|
|
500
|
+
overlay?.modelOverride,
|
|
501
|
+
`${compositionRef}#overlay`,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const rootPermission = normalizePermission(root.permission);
|
|
505
|
+
const overlayPermission = normalizePermission(overlay?.permission);
|
|
506
|
+
|
|
507
|
+
const flattened = flattenCompositionTree(root);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
compositionRef,
|
|
511
|
+
primaryAgentSlug: root.agent.slug,
|
|
512
|
+
primaryInstructions:
|
|
513
|
+
primaryInstructions.length > 0 ? primaryInstructions : null,
|
|
514
|
+
primaryPermission: overlayPermission ?? rootPermission,
|
|
515
|
+
primaryModelOverride: overlayModelOverride ?? rootModelOverride,
|
|
516
|
+
subAgentBindings: flattened.map((n) => ({ ...n })),
|
|
517
|
+
subAgentInstructionsBySlug: collectInstructionsBySlug(root),
|
|
518
|
+
subAgentPermissionBySlug: collectPermissionsBySlug(root),
|
|
519
|
+
subAgentModelOverridesBySlug: collectModelOverridesBySlug(root),
|
|
520
|
+
defaultSkillSlugs: collectSkills(root, { scope: 'root' }),
|
|
521
|
+
allSkillSlugs: collectSkills(root, { scope: 'tree' }),
|
|
522
|
+
defaultToolSelection: collectTools(root, { scope: 'root' }),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
@@ -31,7 +31,9 @@ import {
|
|
|
31
31
|
ManifestSurface,
|
|
32
32
|
OutputSurfaceKind,
|
|
33
33
|
type AgentRunRole,
|
|
34
|
+
type ModelRef,
|
|
34
35
|
} from '@xemahq/kernel-contracts/workflow';
|
|
36
|
+
import type { PermissionMap } from '@xemahq/kernel-contracts/agent-permission';
|
|
35
37
|
import {
|
|
36
38
|
compileManifest,
|
|
37
39
|
type CompiledWorkspaceManifest,
|
|
@@ -104,10 +106,23 @@ export interface CompositionMountLayoutInput {
|
|
|
104
106
|
/**
|
|
105
107
|
* A single manifest-declared sub-agent binding the composition resolved.
|
|
106
108
|
* Mirrors the binding subset the manifest DSL's `ManifestSubAgent` needs.
|
|
109
|
+
*
|
|
110
|
+
* Carries the {@link NodeOverlayFragment} overlay trio (`modelOverride` /
|
|
111
|
+
* `instructions` / `permission`) so a resolved sub-agent's per-node
|
|
112
|
+
* specialization survives the manifest reconstruction. Without these the
|
|
113
|
+
* worker silently DROPPED a delegate's `modelOverride` (the root fix for the
|
|
114
|
+
* worker drift). All optional ⇒ backward-compatible with callers that only
|
|
115
|
+
* supply `slug` / `alias`.
|
|
107
116
|
*/
|
|
108
117
|
export interface CompositionSubAgentBindingInput {
|
|
109
118
|
readonly slug: string;
|
|
110
119
|
readonly alias?: string;
|
|
120
|
+
/** Per-delegate model override — OVERRIDE on the referenced agent's model. */
|
|
121
|
+
readonly modelOverride?: ModelRef;
|
|
122
|
+
/** Per-delegate instructions — APPENDED to the referenced agent's prompt. */
|
|
123
|
+
readonly instructions?: string;
|
|
124
|
+
/** Per-delegate permission — RESTRICT-merged over the agent's permission. */
|
|
125
|
+
readonly permission?: PermissionMap;
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
/**
|
|
@@ -227,6 +242,20 @@ export function compileCompositionWorkspaceManifest(
|
|
|
227
242
|
(binding) => ({
|
|
228
243
|
slug: binding.slug,
|
|
229
244
|
...(binding.alias !== undefined ? { alias: binding.alias } : {}),
|
|
245
|
+
// Pass the overlay trio through to the manifest agent block so a
|
|
246
|
+
// resolved delegate's per-node `modelOverride` / `instructions` /
|
|
247
|
+
// `permission` are NOT dropped on reconstruction (the worker bug).
|
|
248
|
+
// `ManifestSubAgent.defaultModel` is the manifest DSL's name for the
|
|
249
|
+
// kernel `modelOverride` slot.
|
|
250
|
+
...(binding.modelOverride !== undefined
|
|
251
|
+
? { defaultModel: binding.modelOverride }
|
|
252
|
+
: {}),
|
|
253
|
+
...(binding.instructions !== undefined
|
|
254
|
+
? { instructions: binding.instructions }
|
|
255
|
+
: {}),
|
|
256
|
+
...(binding.permission !== undefined
|
|
257
|
+
? { permission: binding.permission }
|
|
258
|
+
: {}),
|
|
230
259
|
}),
|
|
231
260
|
);
|
|
232
261
|
|
|
@@ -309,7 +338,9 @@ function projectOutputSurface(
|
|
|
309
338
|
return {
|
|
310
339
|
kind: surface.kind as OutputSurfaceKind,
|
|
311
340
|
...(surface.port === undefined ? {} : { port: surface.port }),
|
|
312
|
-
...(surface.healthPath === undefined
|
|
341
|
+
...(surface.healthPath === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: { healthPath: surface.healthPath }),
|
|
313
344
|
...(surface.autoOpen === undefined ? {} : { autoOpen: surface.autoOpen }),
|
|
314
345
|
...(surface.mode === undefined ? {} : { mode: surface.mode }),
|
|
315
346
|
...(surface.root === undefined ? {} : { root: surface.root }),
|