@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,147 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ── Skill-bundle-backed TemplateResolver ──
3
+ //
4
+ // Phase 10 (workspace-manifests-api retirement). Seed-file templates used to
5
+ // live in `workspace-manifests-api` (the `workspace_manifest_templates` table)
6
+ // and were fetched by name. They are now SKILL-BUNDLE RESOURCES served by
7
+ // `skill-registry-api` (`GET /skills/bundle`): a biome that ships templates
8
+ // packages them inside a skill bundle under `assets/templates/<name>.hbs`.
9
+ //
10
+ // This factory builds a `TemplateResolver` that:
11
+ // 1. Walks the resolved composition's skill set.
12
+ // 2. Fetches each skill bundle through the injected `fetchBundle` function.
13
+ // 3. Locates the requested template by mapping the bare template name
14
+ // (`demo-seeded-canvas-index.html`) to the resource relPath
15
+ // (`assets/templates/demo-seeded-canvas-index.html.hbs`).
16
+ // 4. Compiles the raw template via the injected `compileTemplate` function
17
+ // and renders it with the seed-file `vars`.
18
+ //
19
+ // Both the bundle-fetch and the Handlebars compile are INJECTED so this
20
+ // Kernel package stays free of an HTTP client and a templating dependency —
21
+ // `agent-session-api` (and, after Step 4, `workflow-runtime-worker`) supply
22
+ // the concrete implementations and share this one resolution algorithm.
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+
25
+ import type { TemplateResolver } from './types';
26
+
27
+ /** The `assets/templates/` directory prefix every template resource sits under. */
28
+ const TEMPLATE_RESOURCE_PREFIX = 'assets/templates/';
29
+ /** The file extension a Handlebars template resource carries inside the bundle. */
30
+ const TEMPLATE_RESOURCE_SUFFIX = '.hbs';
31
+
32
+ /**
33
+ * One resource inside a skill bundle. Mirrors `SkillBundleResourceDto` from
34
+ * the skill-registry-api client without importing it (Kernel stays
35
+ * client-dep-free).
36
+ */
37
+ export interface SkillBundleResource {
38
+ /** Forward-slash path relative to the skill directory. */
39
+ readonly relPath: string;
40
+ /** File bytes, base64-encoded. */
41
+ readonly content: string;
42
+ }
43
+
44
+ /**
45
+ * The slice of a skill bundle this resolver needs. Mirrors
46
+ * `SkillBundleResponseDto` (subset).
47
+ */
48
+ export interface SkillBundle {
49
+ readonly slug: string;
50
+ readonly resources: readonly SkillBundleResource[];
51
+ }
52
+
53
+ /**
54
+ * Fetches one skill bundle by slug. Implemented by the consumer with its
55
+ * `skill-registry-api` Orval client. Must reject (not return null) on a
56
+ * missing bundle — a template referencing a skill that does not resolve is
57
+ * a fail-fast condition.
58
+ */
59
+ export type SkillBundleFetcher = (slug: string) => Promise<SkillBundle>;
60
+
61
+ /**
62
+ * Compiles a raw template string and renders it with `vars`. Injected so
63
+ * the Kernel package does not depend on a concrete templating library.
64
+ */
65
+ export type TemplateCompiler = (
66
+ source: string,
67
+ vars: Readonly<Record<string, unknown>>,
68
+ ) => string;
69
+
70
+ /** Raised when a referenced seed-file template cannot be resolved. */
71
+ export class SkillBundleTemplateNotFoundError extends Error {
72
+ constructor(
73
+ public readonly templateName: string,
74
+ public readonly skillSlugs: readonly string[],
75
+ ) {
76
+ super(
77
+ `Seed-file template "${templateName}" was not found as ` +
78
+ `"${TEMPLATE_RESOURCE_PREFIX}${templateName}${TEMPLATE_RESOURCE_SUFFIX}" ` +
79
+ `in any of the composition's skill bundles ` +
80
+ `[${skillSlugs.join(', ') || '<none>'}]. The biome that owns the ` +
81
+ `template must ship it inside a skill bundle the composition references.`,
82
+ );
83
+ this.name = 'SkillBundleTemplateNotFoundError';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Map a bare seed-file template name to its skill-bundle resource relPath.
89
+ * `demo-seeded-canvas-index.html` → `assets/templates/demo-seeded-canvas-index.html.hbs`.
90
+ */
91
+ export function templateResourceRelPath(templateName: string): string {
92
+ return `${TEMPLATE_RESOURCE_PREFIX}${templateName}${TEMPLATE_RESOURCE_SUFFIX}`;
93
+ }
94
+
95
+ /**
96
+ * Build a `TemplateResolver` backed by the resolved composition's skill
97
+ * bundles.
98
+ *
99
+ * `skillSlugs` is the set of skill slugs attached to the resolved
100
+ * composition (root node + descendants). Bundles are fetched lazily and
101
+ * memoised for the lifetime of the returned resolver, so a session whose
102
+ * manifest references three templates from one skill fetches that bundle
103
+ * exactly once.
104
+ *
105
+ * Fail-fast: a `fetchBundle` rejection propagates; a template name that
106
+ * matches no resource in any bundle throws `SkillBundleTemplateNotFoundError`.
107
+ */
108
+ export function buildSkillBundleTemplateResolver(params: {
109
+ readonly skillSlugs: readonly string[];
110
+ readonly fetchBundle: SkillBundleFetcher;
111
+ readonly compileTemplate: TemplateCompiler;
112
+ }): TemplateResolver {
113
+ const { skillSlugs, fetchBundle, compileTemplate } = params;
114
+ // De-duplicate so the same skill referenced at multiple nodes is fetched once.
115
+ const uniqueSlugs = [...new Set(skillSlugs)];
116
+ const bundleCache = new Map<string, Promise<SkillBundle>>();
117
+
118
+ const loadBundle = (slug: string): Promise<SkillBundle> => {
119
+ const cached = bundleCache.get(slug);
120
+ if (cached) {
121
+ return cached;
122
+ }
123
+ const pending = fetchBundle(slug);
124
+ bundleCache.set(slug, pending);
125
+ return pending;
126
+ };
127
+
128
+ return {
129
+ async resolve(
130
+ name: string,
131
+ vars: Readonly<Record<string, unknown>>,
132
+ ): Promise<string> {
133
+ const relPath = templateResourceRelPath(name);
134
+ for (const slug of uniqueSlugs) {
135
+ const bundle = await loadBundle(slug);
136
+ const resource = bundle.resources.find((r) => r.relPath === relPath);
137
+ if (resource) {
138
+ const source = Buffer.from(resource.content, 'base64').toString(
139
+ 'utf8',
140
+ );
141
+ return compileTemplate(source, vars);
142
+ }
143
+ }
144
+ throw new SkillBundleTemplateNotFoundError(name, uniqueSlugs);
145
+ },
146
+ };
147
+ }
@@ -0,0 +1,443 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ── @xemahq/agent-session-runtime — public types ──
3
+ //
4
+ // The shared abstract substrate beneath two entry points (workflow
5
+ // agent.activity, agent-session bootstrap). Each entry point
6
+ // keeps its own orchestration concerns (Temporal heartbeat /
7
+ // cancellation, MinIO snapshot / resume) but delegates the substrate
8
+ // operations to interfaces declared here:
9
+ //
10
+ // 1. ComposeWorkspaceImage — manifest + bind context → mount plan
11
+ // 2. ApplyWorkspaceImage — push the mount plan to a runtime
12
+ // 3. AgentSessionDriver — drive a single agent turn to completion
13
+ // 4. HarvestDeliverables — read the workspace's emitted manifest
14
+ //
15
+ // All interfaces are runtime-dep-free (no NestJS, no Temporal, no
16
+ // agent-runtime stack details). The concrete HTTP implementations live
17
+ // in `@xemahq/agent-runtime-bridge`; consumers wire them with
18
+ // the right HTTP clients.
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ import type { WorkspaceMountPlan } from '@xemahq/kernel-contracts/agent-workspace';
22
+ import type { CompiledWorkspaceManifest } from '@xemahq/dsl/workspace-manifest';
23
+ import type { AgentRunRole, Briefcase } from '@xemahq/kernel-contracts/workflow';
24
+
25
+ /**
26
+ * Worker allocation handle returned by opencode-pool. Carries the
27
+ * proxy URL + per-allocation password used for `/workspace/*` calls.
28
+ * Passed to every substrate operation.
29
+ */
30
+ export interface WorkerAllocation {
31
+ readonly proxyUrl: string;
32
+ readonly password: string;
33
+ /** Pool allocation id; threaded into request headers for audit. */
34
+ readonly allocationId: string;
35
+ }
36
+
37
+ /**
38
+ * Inputs for `WorkspaceImageComposer.compose()`. Carries everything
39
+ * needed to turn a compiled manifest into a `WorkspaceMountPlan`.
40
+ *
41
+ * Exactly one of `workflowRun` / `interactive` MUST be supplied — the
42
+ * composer uses it to derive the lease key's scope id and to populate
43
+ * the rendered context.json's invocation identity.
44
+ */
45
+ export interface ComposeWorkspaceImageRequest {
46
+ readonly orgId: string;
47
+ readonly projectId: string;
48
+ readonly manifest: CompiledWorkspaceManifest;
49
+ /** Free-form runtime context the composer threads to mount-source extractors. */
50
+ readonly agentContext: Readonly<Record<string, unknown>>;
51
+ readonly workflowRun?: { readonly runId: string; readonly jobRunId: string };
52
+ readonly interactive?: { readonly sessionId: string; readonly turnGen?: number };
53
+ /**
54
+ * Optional overrides for the agent block — used when the workflow
55
+ * action's `with:` block supplies a different role / spec ref than
56
+ * the manifest's defaults (the workflow's short-form sugar).
57
+ */
58
+ readonly agentOverrides?: {
59
+ readonly role?: AgentRunRole;
60
+ readonly deliverableSpecRef?: string;
61
+ };
62
+ /**
63
+ * Resolver for `seedFiles[].source.kind === 'template'` entries. The
64
+ * caller supplies one because templates are skill-bundle resources
65
+ * fetched from skill-registry-api, not content of this kernel-adjacent
66
+ * package. Returns the rendered string (the composer base64-encodes
67
+ * it). Must throw on a missing template — fail-fast, no silent skip.
68
+ */
69
+ readonly templateResolver?: TemplateResolver;
70
+ /**
71
+ * Kernel-shipped System-scope skill slugs to PREPEND to the composer's
72
+ * mounted skill set. Per `.claude/rules/skills-and-composition.md`,
73
+ * skills owned by `SkillSpace.System` are auto-injected into every
74
+ * agent's mounted bundle — they ship from `@xemahq/system-skills`
75
+ * via the System-tier seeder in `skill-registry-api`. The caller
76
+ * (workflow agent.activity, agent-session bootstrap) fetches the
77
+ * list once per session/turn from `skill-registry-api` and forwards
78
+ * it here. The composer prepends these slugs to whatever skills
79
+ * `agentContext.skills` already names, so every agent's mounted
80
+ * skill list starts with System skills then agent-declared ones.
81
+ *
82
+ * Empty / omitted = no System layer is mounted for this call (e.g.
83
+ * unit tests that don't wire skill-registry-api). The composer does
84
+ * NOT fetch them itself — kernel packages do not make HTTP calls.
85
+ */
86
+ readonly systemSkillSlugs?: readonly string[];
87
+ /**
88
+ * Run-scoped Briefcase carried verbatim from `ActionBaseInput.briefcase`.
89
+ * The composer emits MountSource entries for `briefcase.uploads[]` and
90
+ * `briefcase.references[]` (replacing the legacy ad-hoc
91
+ * `agentContext.uploads` / `agentContext.references` plumbing).
92
+ * `briefcase.mcpTools[]` is unioned into the manifest's tool selection
93
+ * by the environment resolver before this composer is called, so the
94
+ * composer itself does not look at that field.
95
+ */
96
+ readonly briefcase?: Briefcase;
97
+ }
98
+
99
+ /**
100
+ * Resolves a named manifest template (a skill-bundle resource under
101
+ * `assets/templates/<name>.hbs`) and renders it with `vars`.
102
+ * Implementations:
103
+ * - `workflow-runtime-worker` agent.activity: skill-bundle fetch +
104
+ * Handlebars render.
105
+ * - `agent-session-api` bootstrap: same shape as agent.activity.
106
+ * See `skill-bundle-template-resolver.ts` for the shared algorithm.
107
+ */
108
+ export interface TemplateResolver {
109
+ resolve(
110
+ name: string,
111
+ vars: Readonly<Record<string, unknown>>,
112
+ ): Promise<string>;
113
+ }
114
+
115
+ /**
116
+ * Inputs for `AgentSessionDriver.driveToCompletion()`.
117
+ *
118
+ * The concrete driver implementation is responsible for translating
119
+ * these into the runtime's wire protocol, parsing the response stream,
120
+ * emitting `SessionFrame` events, and resolving to a `DriveResult`
121
+ * when the agent's turn completes (or rejects on abort/error).
122
+ */
123
+ export interface DrivePromptParams {
124
+ readonly prompt: string;
125
+ readonly agentSlug: string;
126
+ /**
127
+ * Logical platform session id. Required. workspace-proxy uses this as
128
+ * the key into `OpencodeSessionBindings`; passing the SAME id across
129
+ * multiple drives (e.g. self-correction loops) re-attaches to the
130
+ * same opencode session so the agent sees prior conversation. Pass a
131
+ * fresh id (e.g. per pool allocation, per chat session) to start
132
+ * cleanly. Never optional — workspace-proxy returns 400 if missing.
133
+ */
134
+ readonly sessionId: string;
135
+ /**
136
+ * Optional output-format hint forwarded verbatim to the agent
137
+ * runtime. When set with `type: 'json_schema'`, the runtime is
138
+ * expected to validate the assistant's final answer against the
139
+ * schema and surface it on the `done` frame's `structuredOutput`
140
+ * field. When unset, the session runs in plain-text mode and
141
+ * file-based deliverables rely on tool-call writes.
142
+ */
143
+ readonly outputFormat?: AgentOutputFormat;
144
+ /**
145
+ * Optional per-message routing override. When set, the bridge
146
+ * forwards `{ modelID, providerID }` to the runtime's
147
+ * `POST /session/prompt` body so OpenCode routes this prompt to the
148
+ * named virtual provider. Used by interactive sessions to switch
149
+ * model/provider mid-conversation and by workflow steps that pin a
150
+ * model via DSL `with.model`.
151
+ *
152
+ * The Kernel stays runtime-dep-free, so we model the override as the
153
+ * minimum routing record the wire actually needs — the upstream
154
+ * resolver (llm-registry-api) returns the canonical
155
+ * `(modelId, providerSlug, opencodeProviderId)` triple and the caller
156
+ * projects it onto this shape.
157
+ */
158
+ readonly modelOverride?: PromptModelOverride;
159
+ }
160
+
161
+ /**
162
+ * Concrete per-message routing override sent on the
163
+ * `POST /session/prompt` body. workspace-proxy resolves
164
+ * `opencodeProviderId` against the worker's materialized virtual
165
+ * provider map; if no match exists the proxy returns 400.
166
+ *
167
+ * Temperature is intentionally NOT carried per-prompt — it is a
168
+ * SESSION-level setting materialised onto the worker by the control
169
+ * plane (`/control/setup` writes `agent.temperature` into the rendered
170
+ * opencode.jsonc / agent .md), so a per-prompt override would be
171
+ * silently dropped by opencode's `POST /session/:id/message`.
172
+ */
173
+ export interface PromptModelOverride {
174
+ readonly modelId: string;
175
+ readonly providerSlug?: string;
176
+ readonly opencodeProviderId?: string;
177
+ }
178
+
179
+ /**
180
+ * Output-format envelope. Discriminated union so the Kernel stays
181
+ * runtime-dep-free; the concrete bridge implementation projects this
182
+ * onto the runtime's wire shape.
183
+ */
184
+ export type AgentOutputFormat =
185
+ | { readonly type: 'text' }
186
+ | {
187
+ readonly type: 'json_schema';
188
+ readonly schema: Readonly<Record<string, unknown>>;
189
+ /** Inclusive retries the structured-output validator performs before failing. */
190
+ readonly retryCount?: number;
191
+ };
192
+
193
+ /**
194
+ * One frame from the agent runtime's session-event stream. Closed
195
+ * discriminator lets entry-point orchestrators react to specific
196
+ * events (heartbeat, UI pump, tool-pause routing).
197
+ */
198
+ export type SessionFrame =
199
+ | { readonly kind: 'token'; readonly text: string }
200
+ | {
201
+ readonly kind: 'tool-call';
202
+ readonly tool: string;
203
+ readonly args: unknown;
204
+ /**
205
+ * Lifecycle status of the tool invocation. The agent runtime emits a
206
+ * `started` frame the moment the tool part is registered (args may
207
+ * still be empty / partially streamed), then a follow-up
208
+ * `completed` (or `error`) frame for the same `toolCallId` once the
209
+ * model finishes streaming arguments and the tool has run. Consumers
210
+ * that want a single line per tool collapse on `toolCallId`; UIs
211
+ * that animate a tool's lifecycle render the transitions directly.
212
+ */
213
+ readonly status: 'started' | 'completed' | 'error';
214
+ /**
215
+ * Stable correlation id for the tool call across `started`/`completed`
216
+ * frames AND any subsequent `tool-result` frame. Sourced from the
217
+ * runtime's tool-invocation part id (e.g. OpenCode's `part.id`).
218
+ * Optional because some adapters may not expose a stable id; when
219
+ * absent, consumers fall back to keying on `(tool, ordinal)`.
220
+ */
221
+ readonly toolCallId?: string;
222
+ /**
223
+ * Short human-readable hint produced by the runtime (e.g. truncated
224
+ * stderr/stdout for `error`, a one-liner for `completed`). Empty
225
+ * for `started` frames where no output exists yet.
226
+ */
227
+ readonly summary?: string;
228
+ }
229
+ | {
230
+ readonly kind: 'tool-result';
231
+ readonly tool: string;
232
+ /**
233
+ * Full structured tool output as emitted by the runtime. Kept for
234
+ * display + debugging; expensive consumers should clamp before
235
+ * persisting.
236
+ */
237
+ readonly result: unknown;
238
+ /** Same id as the matching `tool-call` frame. */
239
+ readonly toolCallId?: string;
240
+ /**
241
+ * Populated when the tool's `state.error` is non-empty. Mutually
242
+ * exclusive with a meaningful `result`.
243
+ */
244
+ readonly errorMessage?: string;
245
+ }
246
+ | { readonly kind: 'progress'; readonly stepIndex: number; readonly stepCount: number | undefined }
247
+ | { readonly kind: 'error'; readonly message: string }
248
+ | {
249
+ /**
250
+ * Generic "agent tool pause" frame. An active agent session has
251
+ * signalled that one of its tools needs external input before the
252
+ * turn can continue. The activity routes this through the
253
+ * AgentToolInquiryChannel, which materializes an Inquiry, awaits a
254
+ * recipient's reply, and posts the reply back to the runtime so
255
+ * the session resumes.
256
+ *
257
+ * `toolName` is free-form — the Platform bridge's frame-decoder
258
+ * registry is the single source of truth for which raw events
259
+ * map to this frame, and which reply route resumes which tool.
260
+ */
261
+ readonly kind: 'tool_pause';
262
+ /** Tool name (e.g. the runtime's "question" tool). Free-form; not enumerated. */
263
+ readonly toolName: string;
264
+ /**
265
+ * Runtime-supplied request id, opaque to the driver. The platform
266
+ * adapter uses it to route the reply back to the right tool
267
+ * invocation.
268
+ */
269
+ readonly toolRequestId: string;
270
+ /** Joined prompt text shown to the recipient. */
271
+ readonly promptText: string;
272
+ /**
273
+ * Optional structured choices presented by the agent (multiple
274
+ * choice question). `null` when the agent asked an open-ended
275
+ * question.
276
+ */
277
+ readonly choices: readonly string[] | null;
278
+ }
279
+ | {
280
+ readonly kind: 'done';
281
+ readonly turnId: string;
282
+ /**
283
+ * Concatenated final assistant text. Null only when the
284
+ * upstream stream was aborted before completion.
285
+ */
286
+ readonly finalText: string | null;
287
+ /**
288
+ * Validated structured payload, populated only when the prompt
289
+ * was sent with `outputFormat.type === 'json_schema'`. Null
290
+ * otherwise.
291
+ */
292
+ readonly structuredOutput: unknown;
293
+ };
294
+
295
+ export interface DriveResult {
296
+ readonly turnId: string;
297
+ /** Steps completed within the turn (tool calls + token-emit batches). */
298
+ readonly stepCount: number;
299
+ /** True when the runtime completed the turn cleanly; false on abort. */
300
+ readonly completed: boolean;
301
+ /**
302
+ * Final assistant message text. Null when the run was aborted or the
303
+ * upstream stream ended without a `done` frame.
304
+ */
305
+ readonly finalText: string | null;
306
+ /**
307
+ * Validated structured payload. Populated only when the caller
308
+ * passed `DrivePromptParams.outputFormat = { type: 'json_schema', ... }`.
309
+ * Null when no schema was requested or the structured output was
310
+ * not produced.
311
+ */
312
+ readonly structuredOutput: unknown;
313
+ }
314
+
315
+ /**
316
+ * Result of `WorkspaceImageApplier.apply()`. Pass-through of the
317
+ * runtime's apply response (wire shape mirrors
318
+ * `MountApplyEntryResult` from `@xemahq/kernel-contracts/agent-workspace`).
319
+ *
320
+ * Fields exposed let the worker log per-entry status/bytes/error and
321
+ * surface failed entries even when the apply outcome is `applied` (a
322
+ * single failed entry can still mean the agent runs without its
323
+ * bundle).
324
+ */
325
+ export interface ApplyWorkspaceImageResult {
326
+ readonly applyId: string;
327
+ readonly outcome: 'applied' | 'idempotent';
328
+ readonly leaseKey: string;
329
+ readonly entries: ReadonlyArray<{
330
+ readonly mountKey: string;
331
+ readonly slot: string;
332
+ readonly relPath: string;
333
+ readonly status: 'resolved' | 'applied' | 'failed';
334
+ readonly bytesWritten: number;
335
+ readonly errorCode?: string;
336
+ readonly errorMessage?: string;
337
+ }>;
338
+ }
339
+
340
+ /**
341
+ * High-level interface a session-driving caller can use to chain the
342
+ * substrate operations. Concrete implementations live in the entry-
343
+ * point service (workflow agent.activity / agent-session
344
+ * bootstrap) because lifecycle concerns differ.
345
+ */
346
+ export interface AgentSessionRuntime {
347
+ readonly composer: WorkspaceImageComposer;
348
+ readonly applier: WorkspaceImageApplier;
349
+ readonly driver: AgentSessionDriver;
350
+ }
351
+
352
+ export interface WorkspaceImageComposer {
353
+ compose(req: ComposeWorkspaceImageRequest): Promise<WorkspaceMountPlan>;
354
+ }
355
+
356
+ export interface WorkspaceImageApplier {
357
+ apply(
358
+ allocation: WorkerAllocation,
359
+ plan: WorkspaceMountPlan,
360
+ abortSignal?: AbortSignal,
361
+ ): Promise<ApplyWorkspaceImageResult>;
362
+ }
363
+
364
+ /**
365
+ * Async hook called by `driveToCompletion` when the SSE stream emits a
366
+ * `kind: 'tool_pause'` frame. Resolves to the recipient's reply (opaque
367
+ * payload — the platform adapter that delivered the pause frame knows
368
+ * how to map the payload back to the runtime's tool-resume call). The
369
+ * driver forwards the reply to the runtime and the SSE stream resumes.
370
+ *
371
+ * Returning `null` aborts the turn (e.g. operator cancelled the run
372
+ * while the tool was paused). The driver propagates that as an abort
373
+ * to the caller.
374
+ */
375
+ export type OnAgentToolPauseHook = (frame: {
376
+ readonly toolName: string;
377
+ readonly toolRequestId: string;
378
+ readonly promptText: string;
379
+ readonly choices: readonly string[] | null;
380
+ }) => Promise<{ readonly replyPayload: unknown } | null>;
381
+
382
+ /**
383
+ * Optional behavior overrides for `driveToCompletion`. Each field is
384
+ * additive — leaving them undefined preserves the previous fail-fast
385
+ * semantics, so existing callers are unaffected.
386
+ */
387
+ export interface DriveToCompletionOptions {
388
+ /**
389
+ * Routes `kind: 'tool_pause'` frames into a recipient (typically the
390
+ * agent.activity's AgentToolInquiryChannel call which materializes an
391
+ * Inquiry, awaits a reply, and returns it here). The driver POSTs the
392
+ * reply back to the runtime so the session resumes its turn.
393
+ *
394
+ * If a `tool_pause` frame arrives without this hook configured, the
395
+ * driver throws `MissingAgentToolInquiryHookError` (typed; from
396
+ * `@xemahq/agent-tool-inquiry-channel`).
397
+ */
398
+ readonly onAgentToolPause?: OnAgentToolPauseHook;
399
+ /**
400
+ * Called once per yielded frame (every kind, including `done`). Lets
401
+ * callers surface in-flight progress to a UI without forking the
402
+ * driveToCompletion control flow. Errors thrown by the hook are
403
+ * swallowed at the call site — a misbehaving observer never aborts
404
+ * the session drive.
405
+ */
406
+ readonly onFrame?: (frame: SessionFrame) => void;
407
+ /**
408
+ * Called when the underlying runtime reports per-step LLM token
409
+ * usage (e.g. OpenCode's `step_finished` event). Fires once per
410
+ * step. `totalTokens` is the sum of input + output + reasoning
411
+ * (cache reads/writes are NOT included — they are not billable
412
+ * generation tokens). The driver routes runtime-specific token
413
+ * usage shapes through this single normalized hook so the kernel
414
+ * stays runtime-agnostic.
415
+ *
416
+ * Used by the agent.activity to decrement the composition's
417
+ * `tokenBudget` counter and trip a hard abort on breach. Errors
418
+ * thrown by the hook are NOT swallowed: a breach throw MUST
419
+ * propagate so the driver tears down the stream and stops burning
420
+ * tokens. Other thrown errors also surface so they are not
421
+ * silently lost.
422
+ */
423
+ readonly onTokenUsage?: (usage: {
424
+ readonly totalTokens: number;
425
+ readonly inputTokens: number;
426
+ readonly outputTokens: number;
427
+ readonly reasoningTokens: number;
428
+ }) => void;
429
+ }
430
+
431
+ export interface AgentSessionDriver {
432
+ drive(
433
+ allocation: WorkerAllocation,
434
+ params: DrivePromptParams,
435
+ abortSignal: AbortSignal,
436
+ ): AsyncIterable<SessionFrame>;
437
+ driveToCompletion(
438
+ allocation: WorkerAllocation,
439
+ params: DrivePromptParams,
440
+ abortSignal: AbortSignal,
441
+ options?: DriveToCompletionOptions,
442
+ ): Promise<DriveResult>;
443
+ }