@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,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
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -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
|
+
}
|