@xemahq/agent-session-runtime 0.1.1
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 +201 -0
- package/README.md +61 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/composer.d.ts +14 -0
- package/dist/lib/composer.d.ts.map +1 -0
- package/dist/lib/composer.js +595 -0
- package/dist/lib/composer.js.map +1 -0
- package/dist/lib/composition-workspace-manifest.d.ts +44 -0
- package/dist/lib/composition-workspace-manifest.d.ts.map +1 -0
- package/dist/lib/composition-workspace-manifest.js +143 -0
- package/dist/lib/composition-workspace-manifest.js.map +1 -0
- package/dist/lib/dispatch-contract.d.ts +48 -0
- package/dist/lib/dispatch-contract.d.ts.map +1 -0
- package/dist/lib/dispatch-contract.js +8 -0
- package/dist/lib/dispatch-contract.js.map +1 -0
- package/dist/lib/drift-detector.d.ts +40 -0
- package/dist/lib/drift-detector.d.ts.map +1 -0
- package/dist/lib/drift-detector.js +386 -0
- package/dist/lib/drift-detector.js.map +1 -0
- package/dist/lib/environment-resolver.d.ts +84 -0
- package/dist/lib/environment-resolver.d.ts.map +1 -0
- package/dist/lib/environment-resolver.js +181 -0
- package/dist/lib/environment-resolver.js.map +1 -0
- package/dist/lib/errors.d.ts +12 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +27 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/lifecycle-state.d.ts +23 -0
- package/dist/lib/lifecycle-state.d.ts.map +1 -0
- package/dist/lib/lifecycle-state.js +70 -0
- package/dist/lib/lifecycle-state.js.map +1 -0
- package/dist/lib/skill-bundle-template-resolver.d.ts +23 -0
- package/dist/lib/skill-bundle-template-resolver.d.ts.map +1 -0
- package/dist/lib/skill-bundle-template-resolver.js +54 -0
- package/dist/lib/skill-bundle-template-resolver.js.map +1 -0
- package/dist/lib/types.d.ts +141 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +3 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +45 -0
- package/src/index.ts +34 -0
- package/src/lib/composer.ts +1041 -0
- package/src/lib/composition-workspace-manifest.ts +432 -0
- package/src/lib/dispatch-contract.ts +156 -0
- package/src/lib/drift-detector.ts +497 -0
- package/src/lib/environment-resolver.ts +480 -0
- package/src/lib/errors.ts +43 -0
- package/src/lib/lifecycle-state.ts +139 -0
- package/src/lib/skill-bundle-template-resolver.ts +147 -0
- package/src/lib/types.ts +443 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Composition → CompiledWorkspaceManifest reconstruction ──
|
|
3
|
+
//
|
|
4
|
+
// Phase 10 (`workspace-manifests-api` retirement). The composer turns a
|
|
5
|
+
// `CompiledWorkspaceManifest` into a `WorkspaceMountPlan`; before this
|
|
6
|
+
// module the only way to obtain that compiled manifest was to fetch the
|
|
7
|
+
// `WorkspaceManifest` row from `workspace-manifests-api` and run
|
|
8
|
+
// `compileManifest` on it.
|
|
9
|
+
//
|
|
10
|
+
// The Agent Composition primitive now carries the user-data MOUNT LAYOUT
|
|
11
|
+
// on its `workspace.mountLayout` block: the boot-seeder in `llm-registry-api`
|
|
12
|
+
// projects each biome manifest's `extends:`-flattened RAW `spec.mounts` /
|
|
13
|
+
// `spec.seedFiles` / `spec.inputs` there. This module reconstructs a
|
|
14
|
+
// `WorkspaceManifest` envelope from a resolved composition + its
|
|
15
|
+
// `mountLayout`, then runs the SAME `compileManifest` the legacy path used
|
|
16
|
+
// — with the REAL run/session bind inputs, so `${input.x}` tokens in
|
|
17
|
+
// seed-file vars resolve against the caller's inputs, never against a
|
|
18
|
+
// seed-time placeholder.
|
|
19
|
+
//
|
|
20
|
+
// The result is a `CompiledWorkspaceManifest` indistinguishable from the
|
|
21
|
+
// legacy `workspace-manifests-api`-sourced one — the composer interface is
|
|
22
|
+
// unchanged.
|
|
23
|
+
//
|
|
24
|
+
// This module is RUNTIME-AGNOSTIC: it does not import a generated API
|
|
25
|
+
// client. The caller (`agent-session-api` today, `workflow-runtime-worker`
|
|
26
|
+
// next) maps its resolved-composition shape onto the small input
|
|
27
|
+
// interfaces declared here and supplies the bind-input bag.
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
ManifestSurface,
|
|
32
|
+
OutputSurfaceKind,
|
|
33
|
+
type AgentRunRole,
|
|
34
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
35
|
+
import {
|
|
36
|
+
compileManifest,
|
|
37
|
+
type CompiledWorkspaceManifest,
|
|
38
|
+
type ManifestInputDeclaration,
|
|
39
|
+
type ManifestMountsBlock,
|
|
40
|
+
type ManifestOutputSurface,
|
|
41
|
+
type ManifestSeedFile,
|
|
42
|
+
type ManifestSubAgent,
|
|
43
|
+
type WorkspaceManifest as DslWorkspaceManifest,
|
|
44
|
+
type WorkspaceManifestSpec,
|
|
45
|
+
} from '@xemahq/dsl/workspace-manifest';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Re-export so callers that consume this module's `compile…` result do not
|
|
49
|
+
* need a second import from `@xemahq/dsl/workspace-manifest`.
|
|
50
|
+
*/
|
|
51
|
+
export type { CompiledWorkspaceManifest };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Raised when a resolved composition cannot be reconstructed into a
|
|
55
|
+
* compilable workspace manifest — a missing `mountLayout`, a malformed
|
|
56
|
+
* composition ref, or a `compileManifest` failure. Fail-fast: a session /
|
|
57
|
+
* run cannot bootstrap a `/workspace/` tree without a valid mount layout.
|
|
58
|
+
*/
|
|
59
|
+
export class CompositionMountLayoutError extends Error {
|
|
60
|
+
constructor(compositionRef: string, detail: string) {
|
|
61
|
+
super(
|
|
62
|
+
`Cannot derive a workspace mount layout from Agent Composition ` +
|
|
63
|
+
`"${compositionRef}": ${detail}`,
|
|
64
|
+
);
|
|
65
|
+
this.name = 'CompositionMountLayoutError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The agent run-config the seeder projects from the source manifest's
|
|
71
|
+
* `spec.agent` block. Mirrors `CompositionAgentRunConfigDto` without
|
|
72
|
+
* importing the generated client.
|
|
73
|
+
*/
|
|
74
|
+
export interface CompositionAgentRunConfigInput {
|
|
75
|
+
/** Phase key. */
|
|
76
|
+
readonly phase: string;
|
|
77
|
+
/** Canonical `AgentRunRole` — drives renderer + system-overlay framing. */
|
|
78
|
+
readonly role: string;
|
|
79
|
+
/**
|
|
80
|
+
* Deliverable-spec ref. May carry a `${input.x}` token — resolved
|
|
81
|
+
* downstream against the bind inputs.
|
|
82
|
+
*/
|
|
83
|
+
readonly deliverableSpecRef?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The `extends:`-flattened RAW user-data mount layout carried on a
|
|
88
|
+
* composition's `workspace.mountLayout` block. The seeder owns the wire
|
|
89
|
+
* shape; `compileManifest`'s Zod schema re-validates `mounts` / `seedFiles`
|
|
90
|
+
* / `inputs` against the manifest DSL's authoritative schema, so the loose
|
|
91
|
+
* typing here is checked downstream, not silently trusted.
|
|
92
|
+
*/
|
|
93
|
+
export interface CompositionMountLayoutInput {
|
|
94
|
+
/** Raw `spec.mounts` block — manifest DSL mount-declaration map. */
|
|
95
|
+
readonly mounts: Readonly<Record<string, unknown>>;
|
|
96
|
+
/** Projected `spec.agent` run-config. */
|
|
97
|
+
readonly agentRunConfig: CompositionAgentRunConfigInput;
|
|
98
|
+
/** Raw `spec.seedFiles` array — manifest DSL seed-file entries. */
|
|
99
|
+
readonly seedFiles: readonly unknown[];
|
|
100
|
+
/** Raw `spec.inputs` block — manifest DSL input-declaration map. */
|
|
101
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A single manifest-declared sub-agent binding the composition resolved.
|
|
106
|
+
* Mirrors the binding subset the manifest DSL's `ManifestSubAgent` needs.
|
|
107
|
+
*/
|
|
108
|
+
export interface CompositionSubAgentBindingInput {
|
|
109
|
+
readonly slug: string;
|
|
110
|
+
readonly alias?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The composition's `workspace.outputSurface` block projected onto the
|
|
115
|
+
* shape the manifest DSL accepts. Mirrors `ManifestOutputSurface` from
|
|
116
|
+
* `@xemahq/dsl/workspace-manifest` but restated here so callers do not
|
|
117
|
+
* need a second import.
|
|
118
|
+
*
|
|
119
|
+
* Carried through the reconstructed manifest's `spec.outputSurface` so
|
|
120
|
+
* the compiled output's `CompiledManifestOutputSurface` matches the
|
|
121
|
+
* authoritative source — drift detection and runtime defaults read the
|
|
122
|
+
* same kind/root/port the bundle-apply path reads from
|
|
123
|
+
* `composition.workspace.outputSurface`.
|
|
124
|
+
*/
|
|
125
|
+
export interface CompositionOutputSurfaceInput {
|
|
126
|
+
readonly kind: 'none' | 'web' | 'static' | 'app' | 'tunnel';
|
|
127
|
+
readonly port?: number;
|
|
128
|
+
readonly healthPath?: string;
|
|
129
|
+
readonly autoOpen?: boolean;
|
|
130
|
+
readonly mode?: 'single' | 'multi';
|
|
131
|
+
readonly root?: string;
|
|
132
|
+
readonly defaultDocument?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The minimal resolved-composition projection this module reconstructs a
|
|
137
|
+
* `WorkspaceManifest` from. The caller maps its richer resolved-composition
|
|
138
|
+
* shape onto this — keeping the runtime package free of any API client.
|
|
139
|
+
*/
|
|
140
|
+
export interface CompositionManifestSource {
|
|
141
|
+
/** Version-pinned `slug@version` of the resolved composition. */
|
|
142
|
+
readonly compositionRef: string;
|
|
143
|
+
/** Root node's agent slug — the manifest's primary agent. */
|
|
144
|
+
readonly primaryAgentSlug: string;
|
|
145
|
+
/** Manifest-declared sub-agent bindings (descendant nodes). */
|
|
146
|
+
readonly subAgentBindings: readonly CompositionSubAgentBindingInput[];
|
|
147
|
+
/**
|
|
148
|
+
* The composition's `workspace.mountLayout` block. `undefined` when the
|
|
149
|
+
* composition declared no `workspace` block or the seeder did not project
|
|
150
|
+
* a layout — a fail-fast condition.
|
|
151
|
+
*/
|
|
152
|
+
readonly mountLayout: CompositionMountLayoutInput | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* The composition's `workspace.outputSurface` block. Threaded into the
|
|
155
|
+
* reconstructed manifest's `spec.outputSurface` so the compiled
|
|
156
|
+
* manifest's `outputSurface` matches the source-of-truth (drift
|
|
157
|
+
* detection + non-bundle consumers like `workflow-runtime-worker` read
|
|
158
|
+
* the same value the bundle-apply path reads). `undefined` when the
|
|
159
|
+
* composition declared no surface — the DSL compile then yields the
|
|
160
|
+
* `{ kind: 'none' }` default.
|
|
161
|
+
*/
|
|
162
|
+
readonly outputSurface?: CompositionOutputSurfaceInput;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The implicit inputs every reconstructed manifest is bound against, on top
|
|
167
|
+
* of the caller-supplied explicit inputs.
|
|
168
|
+
*
|
|
169
|
+
* `sessionId` is the run-scoped execution identity. The agent-session-api
|
|
170
|
+
* caller supplies the `Session.id`; the workflow-runtime-worker caller —
|
|
171
|
+
* which has no session — supplies the workflow run id. Both are
|
|
172
|
+
* run-scoped; only manifests that declare a `sessionId` input ever read
|
|
173
|
+
* it, and no first-party manifest does today. Optional so a caller with
|
|
174
|
+
* no run-scoped id at all can omit it.
|
|
175
|
+
*/
|
|
176
|
+
export interface CompositionManifestImplicitInputs {
|
|
177
|
+
readonly orgId: string;
|
|
178
|
+
readonly projectId: string;
|
|
179
|
+
readonly sessionId?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Manifest input value primitives accepted by the manifest DSL. */
|
|
183
|
+
type ManifestInputValue =
|
|
184
|
+
| string
|
|
185
|
+
| number
|
|
186
|
+
| boolean
|
|
187
|
+
| readonly string[]
|
|
188
|
+
| readonly Readonly<Record<string, unknown>>[];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Reconstruct a `WorkspaceManifest` envelope from a resolved composition
|
|
192
|
+
* and run `compileManifest` against the supplied bind inputs.
|
|
193
|
+
*
|
|
194
|
+
* The reconstructed envelope carries NO `extends:` — the boot-seeder
|
|
195
|
+
* already flattened the source manifest's `extends:` chain before
|
|
196
|
+
* projecting `mountLayout`, so the layout is the effective post-merge
|
|
197
|
+
* shape.
|
|
198
|
+
*
|
|
199
|
+
* `bindInputs` is the resolved manifest input bag (explicit inputs ∪
|
|
200
|
+
* implicit inputs), built by {@link buildCompositionManifestBindInputs}.
|
|
201
|
+
* The compile fails fast on an unresolved required input or an enum
|
|
202
|
+
* violation — exactly the legacy behaviour.
|
|
203
|
+
*/
|
|
204
|
+
export function compileCompositionWorkspaceManifest(
|
|
205
|
+
source: CompositionManifestSource,
|
|
206
|
+
bindInputs: Readonly<Record<string, unknown>>,
|
|
207
|
+
): CompiledWorkspaceManifest {
|
|
208
|
+
const mountLayout = source.mountLayout;
|
|
209
|
+
if (!mountLayout) {
|
|
210
|
+
throw new CompositionMountLayoutError(
|
|
211
|
+
source.compositionRef,
|
|
212
|
+
'the composition declares no `workspace.mountLayout` block — its ' +
|
|
213
|
+
'source workspace manifest was not projected by the llm-registry-api ' +
|
|
214
|
+
'composition seeder. Re-seed compositions (boot llm-registry-api) or ' +
|
|
215
|
+
'fix the source manifest.',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const [slug, version] = splitCompositionRef(source.compositionRef);
|
|
220
|
+
|
|
221
|
+
// Sub-agents the composition resolved become the manifest agent block's
|
|
222
|
+
// `subAgents[]`. The composer + EnvironmentResolver surface these on the
|
|
223
|
+
// resolved environment snapshot (drift detection). The intrinsic floor
|
|
224
|
+
// is still merged separately downstream — this is the manifest-declared
|
|
225
|
+
// layer only, exactly as a legacy manifest's `agent.subAgents` was.
|
|
226
|
+
const subAgents: ManifestSubAgent[] = source.subAgentBindings.map(
|
|
227
|
+
(binding) => ({
|
|
228
|
+
slug: binding.slug,
|
|
229
|
+
...(binding.alias !== undefined ? { alias: binding.alias } : {}),
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Raw fragments carried verbatim by the seeder. They are typed loosely
|
|
234
|
+
// on the wire (the seeder owns the shape); `compileManifest`'s Zod schema
|
|
235
|
+
// below re-validates them against the manifest DSL's authoritative
|
|
236
|
+
// `WorkspaceManifestSchema`, so the casts here are checked downstream,
|
|
237
|
+
// not silently trusted.
|
|
238
|
+
const outputSurface = projectOutputSurface(source.outputSurface);
|
|
239
|
+
const spec: WorkspaceManifestSpec = {
|
|
240
|
+
mounts: mountLayout.mounts as unknown as ManifestMountsBlock,
|
|
241
|
+
agent: {
|
|
242
|
+
slug: source.primaryAgentSlug,
|
|
243
|
+
phase: mountLayout.agentRunConfig.phase,
|
|
244
|
+
role: mountLayout.agentRunConfig.role as AgentRunRole,
|
|
245
|
+
...(mountLayout.agentRunConfig.deliverableSpecRef !== undefined
|
|
246
|
+
? { deliverableSpecRef: mountLayout.agentRunConfig.deliverableSpecRef }
|
|
247
|
+
: {}),
|
|
248
|
+
...(subAgents.length > 0 ? { subAgents } : {}),
|
|
249
|
+
},
|
|
250
|
+
...(Object.keys(mountLayout.inputs).length > 0
|
|
251
|
+
? {
|
|
252
|
+
inputs: mountLayout.inputs as unknown as Record<
|
|
253
|
+
string,
|
|
254
|
+
ManifestInputDeclaration
|
|
255
|
+
>,
|
|
256
|
+
}
|
|
257
|
+
: {}),
|
|
258
|
+
...(mountLayout.seedFiles.length > 0
|
|
259
|
+
? {
|
|
260
|
+
seedFiles:
|
|
261
|
+
mountLayout.seedFiles as unknown as readonly ManifestSeedFile[],
|
|
262
|
+
}
|
|
263
|
+
: {}),
|
|
264
|
+
...(outputSurface === undefined ? {} : { outputSurface }),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const envelope: DslWorkspaceManifest = {
|
|
268
|
+
apiVersion: 'xema.dev/workspace/v1',
|
|
269
|
+
kind: 'WorkspaceManifest',
|
|
270
|
+
metadata: {
|
|
271
|
+
slug,
|
|
272
|
+
version,
|
|
273
|
+
// Compositions resolved for a session bootstrap are, by definition,
|
|
274
|
+
// agent-session compatible — the runtime EnvironmentResolver still
|
|
275
|
+
// re-asserts `surfaceCompat` against the AGENT_SESSION surface.
|
|
276
|
+
surfaceCompat: [ManifestSurface.AGENT_SESSION],
|
|
277
|
+
},
|
|
278
|
+
spec,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const result = compileManifest(envelope, bindInputs);
|
|
282
|
+
if (!result.ok) {
|
|
283
|
+
throw new CompositionMountLayoutError(
|
|
284
|
+
source.compositionRef,
|
|
285
|
+
`reconstructed manifest failed to compile: ${result.errors
|
|
286
|
+
.map((e) => `${e.path}: ${e.message}`)
|
|
287
|
+
.join('; ')}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return result.compiled;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Project the composition's `workspace.outputSurface` block (carried on
|
|
295
|
+
* the runtime-agnostic `CompositionManifestSource`) into the manifest DSL's
|
|
296
|
+
* `ManifestOutputSurface` shape so the reconstructed envelope's
|
|
297
|
+
* `spec.outputSurface` carries the same kind/root/port as the source. The
|
|
298
|
+
* `kind` value strings match the DSL enum 1:1 (`none` / `web` / `static` /
|
|
299
|
+
* `app` / `tunnel`) so the cast is exact, not coerced.
|
|
300
|
+
*
|
|
301
|
+
* Returns `undefined` when the composition declared no surface — the DSL
|
|
302
|
+
* `compileManifest` then defaults the compiled `outputSurface` to
|
|
303
|
+
* `{ kind: 'none', autoOpen: false, mode: 'single' }`.
|
|
304
|
+
*/
|
|
305
|
+
function projectOutputSurface(
|
|
306
|
+
surface: CompositionOutputSurfaceInput | undefined,
|
|
307
|
+
): ManifestOutputSurface | undefined {
|
|
308
|
+
if (!surface) return undefined;
|
|
309
|
+
return {
|
|
310
|
+
kind: surface.kind as OutputSurfaceKind,
|
|
311
|
+
...(surface.port === undefined ? {} : { port: surface.port }),
|
|
312
|
+
...(surface.healthPath === undefined ? {} : { healthPath: surface.healthPath }),
|
|
313
|
+
...(surface.autoOpen === undefined ? {} : { autoOpen: surface.autoOpen }),
|
|
314
|
+
...(surface.mode === undefined ? {} : { mode: surface.mode }),
|
|
315
|
+
...(surface.root === undefined ? {} : { root: surface.root }),
|
|
316
|
+
...(surface.defaultDocument === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { defaultDocument: surface.defaultDocument }),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Build the manifest bind-input bag from the caller's explicit input bag
|
|
324
|
+
* and the implicit inputs, validated against the composition's declared
|
|
325
|
+
* `mountLayout.inputs`.
|
|
326
|
+
*
|
|
327
|
+
* • an undeclared explicit input key fails fast;
|
|
328
|
+
* • a non-primitive / mixed-array input value fails fast;
|
|
329
|
+
* • a declared input with neither an explicit value nor an implicit
|
|
330
|
+
* value is simply omitted — the compiler then applies the input's
|
|
331
|
+
* own `default` (or fails fast if it is `required`).
|
|
332
|
+
*
|
|
333
|
+
* Returns `{}` when the composition declared no inputs.
|
|
334
|
+
*/
|
|
335
|
+
export function buildCompositionManifestBindInputs(
|
|
336
|
+
source: CompositionManifestSource,
|
|
337
|
+
explicitInputs: Record<string, unknown> | null,
|
|
338
|
+
implicitInputs: CompositionManifestImplicitInputs,
|
|
339
|
+
): Readonly<Record<string, unknown>> {
|
|
340
|
+
const declared = new Set(Object.keys(source.mountLayout?.inputs ?? {}));
|
|
341
|
+
if (declared.size === 0) {
|
|
342
|
+
return {};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (
|
|
346
|
+
explicitInputs !== null &&
|
|
347
|
+
(typeof explicitInputs !== 'object' || Array.isArray(explicitInputs))
|
|
348
|
+
) {
|
|
349
|
+
throw new CompositionMountLayoutError(
|
|
350
|
+
source.compositionRef,
|
|
351
|
+
'explicit manifest inputs must be an object when provided',
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const explicit = explicitInputs ?? {};
|
|
355
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
356
|
+
if (!declared.has(key)) {
|
|
357
|
+
throw new CompositionMountLayoutError(
|
|
358
|
+
source.compositionRef,
|
|
359
|
+
`manifest inputs contain undeclared key "${key}"`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
assertManifestInputValue(source.compositionRef, key, value);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const implicit: Record<string, ManifestInputValue> = {
|
|
366
|
+
orgId: implicitInputs.orgId,
|
|
367
|
+
projectId: implicitInputs.projectId,
|
|
368
|
+
...(implicitInputs.sessionId !== undefined
|
|
369
|
+
? { sessionId: implicitInputs.sessionId }
|
|
370
|
+
: {}),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const out: Record<string, unknown> = {};
|
|
374
|
+
for (const key of declared) {
|
|
375
|
+
if (Object.hasOwn(explicit, key)) {
|
|
376
|
+
out[key] = explicit[key];
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (Object.hasOwn(implicit, key)) {
|
|
380
|
+
out[key] = implicit[key];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Split a `slug@version` composition ref; fail fast on a bare slug. */
|
|
387
|
+
function splitCompositionRef(ref: string): [slug: string, version: string] {
|
|
388
|
+
const at = ref.lastIndexOf('@');
|
|
389
|
+
if (at <= 0 || at === ref.length - 1) {
|
|
390
|
+
throw new CompositionMountLayoutError(
|
|
391
|
+
ref,
|
|
392
|
+
'expected a version-pinned `slug@version` composition ref — the ' +
|
|
393
|
+
'session resolver always pins the resolved version',
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return [ref.slice(0, at), ref.slice(at + 1)];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Manifest inputs flow into seed-file templates via Handlebars
|
|
401
|
+
* interpolation. Non-primitives produce `[object Object]` silently;
|
|
402
|
+
* mixed-type arrays break string-array inputs. Reject both at the
|
|
403
|
+
* boundary so the error surfaces near the cause.
|
|
404
|
+
*/
|
|
405
|
+
function assertManifestInputValue(
|
|
406
|
+
compositionRef: string,
|
|
407
|
+
key: string,
|
|
408
|
+
value: unknown,
|
|
409
|
+
): void {
|
|
410
|
+
if (
|
|
411
|
+
value !== null &&
|
|
412
|
+
typeof value !== 'string' &&
|
|
413
|
+
typeof value !== 'number' &&
|
|
414
|
+
typeof value !== 'boolean' &&
|
|
415
|
+
!Array.isArray(value)
|
|
416
|
+
) {
|
|
417
|
+
throw new CompositionMountLayoutError(
|
|
418
|
+
compositionRef,
|
|
419
|
+
`manifest input "${key}" must be a primitive ` +
|
|
420
|
+
`(string, number, boolean, string[]) — got ${typeof value}`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
Array.isArray(value) &&
|
|
425
|
+
!value.every((entry) => typeof entry === 'string')
|
|
426
|
+
) {
|
|
427
|
+
throw new CompositionMountLayoutError(
|
|
428
|
+
compositionRef,
|
|
429
|
+
`manifest input "${key}" is an array — every element must be a string`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Agent-session dispatch wire contract ──
|
|
3
|
+
//
|
|
4
|
+
// Typed shapes the workflow-runtime-worker activity passes to (and
|
|
5
|
+
// receives from) `agent-session-api`'s
|
|
6
|
+
// `POST /internal/sessions/:id/dispatch` endpoint.
|
|
7
|
+
//
|
|
8
|
+
// These live in the Kernel package so the worker can depend on them
|
|
9
|
+
// without pulling in the (large) Orval client at type-check time. The
|
|
10
|
+
// generated Orval models in `@xemahq/agent-session-api-client` MUST be
|
|
11
|
+
// structurally compatible — CI enforces it via a type-equality check
|
|
12
|
+
// in `biomes/agent-runtime/api/agent-session-api/test/contract-compat.test.ts`.
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
import type { ArtifactRef } from '@xemahq/kernel-contracts/workflow';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mode discriminator on the dispatch envelope. Drives whether the
|
|
19
|
+
* service creates a fresh Session row or resumes an existing one.
|
|
20
|
+
*/
|
|
21
|
+
export const AgentSessionDispatchMode = {
|
|
22
|
+
/** Iter 1: mint a new Session, provision a worker, run one turn, pause. */
|
|
23
|
+
create_and_run: 'create_and_run',
|
|
24
|
+
/** Iter N+1: resume an existing Session, mount revision inputs, run one turn, pause. */
|
|
25
|
+
resume_and_run: 'resume_and_run',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export type AgentSessionDispatchModeValue =
|
|
29
|
+
(typeof AgentSessionDispatchMode)[keyof typeof AgentSessionDispatchMode];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A single revision input file to seed into `/workspace/inputs/...`
|
|
33
|
+
* BEFORE the agent's turn starts. The dispatch service writes the file
|
|
34
|
+
* via workspace-proxy's mount-apply call (one round-trip), so the
|
|
35
|
+
* agent's first read of the workspace sees it.
|
|
36
|
+
*
|
|
37
|
+
* The path is RELATIVE to `/workspace/inputs/` and validated against
|
|
38
|
+
* path-traversal (`..`, absolute paths, symlinks). Slot membership is
|
|
39
|
+
* enforced — only paths inside the manifest's declared `inputs` slot
|
|
40
|
+
* can be written.
|
|
41
|
+
*/
|
|
42
|
+
export interface RevisionInput {
|
|
43
|
+
/**
|
|
44
|
+
* Path under `/workspace/inputs/`, e.g. `revisions/revision-2-<iso>.md`.
|
|
45
|
+
* Validated: no leading slash, no `..`, must match the declared
|
|
46
|
+
* inputs-slot subpath whitelist.
|
|
47
|
+
*/
|
|
48
|
+
readonly path: string;
|
|
49
|
+
/** Base64-encoded file bytes. UTF-8 markdown is the common case. */
|
|
50
|
+
readonly contentBase64: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Per-iteration prompt-side context the worker hands to the agent.
|
|
55
|
+
* Kept small on purpose: the bulk of the iteration's reviewer-feedback
|
|
56
|
+
* lives in the revision file, not here. The agent reads the file path
|
|
57
|
+
* out of `revisionFilePath` and `read`s it from the workspace.
|
|
58
|
+
*/
|
|
59
|
+
export interface DispatchPromptContext {
|
|
60
|
+
/** Free-form task statement (same shape as `agentContext.prompt` today). */
|
|
61
|
+
readonly prompt: string;
|
|
62
|
+
/**
|
|
63
|
+
* Path inside the workspace where the agent will find the actionable
|
|
64
|
+
* revision directive for this iteration. Omitted on iter 1.
|
|
65
|
+
* Example: `inputs/revisions/revision-2-2026-05-19T12-30-00Z.md`.
|
|
66
|
+
*/
|
|
67
|
+
readonly revisionFilePath?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Iteration number for telemetry / heartbeat. `1` for create-and-run,
|
|
70
|
+
* `>=2` for resume-and-run.
|
|
71
|
+
*/
|
|
72
|
+
readonly iteration: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `mode: create_and_run` payload. The service creates a Session, calls
|
|
77
|
+
* pool acquire, then drives one turn.
|
|
78
|
+
*/
|
|
79
|
+
export interface CreateAndRunDispatch {
|
|
80
|
+
readonly mode: typeof AgentSessionDispatchMode.create_and_run;
|
|
81
|
+
/** ownerKind is forced to `pipeline_run` on this code path. */
|
|
82
|
+
readonly pipelineRunId: string;
|
|
83
|
+
readonly jobRunId: string;
|
|
84
|
+
/** Stable spine identity within the workflow run. */
|
|
85
|
+
readonly jobKey: string;
|
|
86
|
+
/**
|
|
87
|
+
* Agent Composition ref (`slug` = latest published, or `slug@version`)
|
|
88
|
+
* the new pipeline-owned session should bootstrap from. The dispatch
|
|
89
|
+
* service stamps this onto `Session.compositionRef`; the session
|
|
90
|
+
* lifecycle resolves it via llm-registry-api's `GET /compositions/
|
|
91
|
+
* resolve/:ref` for both the agent/skill/tool tree and the workspace
|
|
92
|
+
* mount layout.
|
|
93
|
+
*/
|
|
94
|
+
readonly compositionRef: string;
|
|
95
|
+
readonly agentSlug: string;
|
|
96
|
+
readonly promptContext: DispatchPromptContext;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `mode: resume_and_run` payload. The service resumes the named
|
|
101
|
+
* Session (must be `paused`), writes the revision inputs into the
|
|
102
|
+
* workspace as part of mount-apply, then drives one turn.
|
|
103
|
+
*/
|
|
104
|
+
export interface ResumeAndRunDispatch {
|
|
105
|
+
readonly mode: typeof AgentSessionDispatchMode.resume_and_run;
|
|
106
|
+
readonly sessionId: string;
|
|
107
|
+
readonly revisionInputs: readonly RevisionInput[];
|
|
108
|
+
readonly promptContext: DispatchPromptContext;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type AgentSessionDispatchRequest =
|
|
112
|
+
| CreateAndRunDispatch
|
|
113
|
+
| ResumeAndRunDispatch;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Response from the dispatch endpoint. Carries the harvested outputs
|
|
117
|
+
* from the turn AND the session's new (post-pause) snapshot generation
|
|
118
|
+
* so the caller can correlate the iteration with its MinIO blob.
|
|
119
|
+
*
|
|
120
|
+
* The `sessionId` is echoed back so a `create_and_run` caller can
|
|
121
|
+
* record it on Temporal state for the next iteration.
|
|
122
|
+
*/
|
|
123
|
+
export interface AgentSessionDispatchResponse {
|
|
124
|
+
readonly sessionId: string;
|
|
125
|
+
readonly snapshotGeneration: number;
|
|
126
|
+
readonly pausedAt: string;
|
|
127
|
+
/** Harvested deliverables (artifact refs emitted by the agent's turn). */
|
|
128
|
+
readonly deliverables: readonly ArtifactRef[];
|
|
129
|
+
/**
|
|
130
|
+
* Free-form structured output (e.g. inquiry answers). Same shape as
|
|
131
|
+
* today's `RawAgentActivityOutput.structuredOutput`.
|
|
132
|
+
*/
|
|
133
|
+
readonly structuredOutput: unknown;
|
|
134
|
+
/**
|
|
135
|
+
* Markdown response artifact ref (if the manifest declared one).
|
|
136
|
+
* Same shape as today's `RawAgentActivityOutput.response`.
|
|
137
|
+
*/
|
|
138
|
+
readonly response: ArtifactRef | null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Body for `POST /internal/sessions/:id/inputs/append`. Used by
|
|
143
|
+
* out-of-band callers (e.g. interactive-session UI file uploads) when
|
|
144
|
+
* the fast path through mount-apply isn't applicable. Internal-only;
|
|
145
|
+
* never exposed through the public Gateway.
|
|
146
|
+
*/
|
|
147
|
+
export interface InputsAppendRequest {
|
|
148
|
+
readonly path: string;
|
|
149
|
+
readonly contentBase64: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface InputsAppendResponse {
|
|
153
|
+
readonly written: boolean;
|
|
154
|
+
/** SHA-256 of the bytes as written. Useful for idempotency checks. */
|
|
155
|
+
readonly sha256: string;
|
|
156
|
+
}
|