@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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +61 -0
  3. package/dist/index.d.ts +10 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +26 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/composer.d.ts +14 -0
  8. package/dist/lib/composer.d.ts.map +1 -0
  9. package/dist/lib/composer.js +595 -0
  10. package/dist/lib/composer.js.map +1 -0
  11. package/dist/lib/composition-workspace-manifest.d.ts +44 -0
  12. package/dist/lib/composition-workspace-manifest.d.ts.map +1 -0
  13. package/dist/lib/composition-workspace-manifest.js +143 -0
  14. package/dist/lib/composition-workspace-manifest.js.map +1 -0
  15. package/dist/lib/dispatch-contract.d.ts +48 -0
  16. package/dist/lib/dispatch-contract.d.ts.map +1 -0
  17. package/dist/lib/dispatch-contract.js +8 -0
  18. package/dist/lib/dispatch-contract.js.map +1 -0
  19. package/dist/lib/drift-detector.d.ts +40 -0
  20. package/dist/lib/drift-detector.d.ts.map +1 -0
  21. package/dist/lib/drift-detector.js +386 -0
  22. package/dist/lib/drift-detector.js.map +1 -0
  23. package/dist/lib/environment-resolver.d.ts +84 -0
  24. package/dist/lib/environment-resolver.d.ts.map +1 -0
  25. package/dist/lib/environment-resolver.js +181 -0
  26. package/dist/lib/environment-resolver.js.map +1 -0
  27. package/dist/lib/errors.d.ts +12 -0
  28. package/dist/lib/errors.d.ts.map +1 -0
  29. package/dist/lib/errors.js +27 -0
  30. package/dist/lib/errors.js.map +1 -0
  31. package/dist/lib/lifecycle-state.d.ts +23 -0
  32. package/dist/lib/lifecycle-state.d.ts.map +1 -0
  33. package/dist/lib/lifecycle-state.js +70 -0
  34. package/dist/lib/lifecycle-state.js.map +1 -0
  35. package/dist/lib/skill-bundle-template-resolver.d.ts +23 -0
  36. package/dist/lib/skill-bundle-template-resolver.d.ts.map +1 -0
  37. package/dist/lib/skill-bundle-template-resolver.js +54 -0
  38. package/dist/lib/skill-bundle-template-resolver.js.map +1 -0
  39. package/dist/lib/types.d.ts +141 -0
  40. package/dist/lib/types.d.ts.map +1 -0
  41. package/dist/lib/types.js +3 -0
  42. package/dist/lib/types.js.map +1 -0
  43. package/package.json +45 -0
  44. package/src/index.ts +34 -0
  45. package/src/lib/composer.ts +1041 -0
  46. package/src/lib/composition-workspace-manifest.ts +432 -0
  47. package/src/lib/dispatch-contract.ts +156 -0
  48. package/src/lib/drift-detector.ts +497 -0
  49. package/src/lib/environment-resolver.ts +480 -0
  50. package/src/lib/errors.ts +43 -0
  51. package/src/lib/lifecycle-state.ts +139 -0
  52. package/src/lib/skill-bundle-template-resolver.ts +147 -0
  53. 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
+ }