@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,497 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ── Environment drift detector ──
3
+ //
4
+ // Diffs two `ResolvedEnvironment` snapshots and emits a structured
5
+ // drift report. Used by agent-session resume to compare the
6
+ // snapshot from the previous run against a freshly-resolved snapshot
7
+ // at resume time — re-resolve is the source of truth, drift is what
8
+ // surfaces to the operator.
9
+ //
10
+ // Drift kinds are a closed enum (`DriftKind`). Severity is `info |
11
+ // warning` — purely advisory: NO drift blocks a resume. Resume always
12
+ // proceeds with the freshly-resolved environment; the drift list is
13
+ // surfaced to the operator (timeline entry + the `SessionResumed`
14
+ // degradation toast) so they see what changed. `warning` flags the
15
+ // changes worth a second look (a removed sub-agent/skill/credential, a
16
+ // changed agent, a tightened permission); `info` is benign drift
17
+ // (version bumps, additions, value tweaks).
18
+ // ═══════════════════════════════════════════════════════════════════════════
19
+
20
+ import type { ToolSelectionEntry } from '@xemahq/kernel-contracts/mcp-tool';
21
+ import type {
22
+ CompiledManifestCredential,
23
+ CompiledManifestSkillRef,
24
+ SubAgentBinding,
25
+ } from '@xemahq/kernel-contracts/workflow';
26
+
27
+ import type { ResolvedEnvironment } from './environment-resolver';
28
+
29
+ export enum DriftKind {
30
+ AGENT_CHANGED = 'agent-changed',
31
+ ROLE_CHANGED = 'role-changed',
32
+ MANIFEST_VERSION_CHANGED = 'manifest-version-changed',
33
+ MODEL_CHANGED = 'model-changed',
34
+ SUBAGENT_ADDED = 'subagent-added',
35
+ SUBAGENT_REMOVED = 'subagent-removed',
36
+ SUBAGENT_MODEL_CHANGED = 'subagent-model-changed',
37
+ SKILL_ADDED = 'skill-added',
38
+ SKILL_REMOVED = 'skill-removed',
39
+ SKILL_VERSION_CHANGED = 'skill-version-changed',
40
+ TOOL_SELECTION_ADDED = 'tool-selection-added',
41
+ TOOL_SELECTION_REMOVED = 'tool-selection-removed',
42
+ CREDENTIAL_ADDED = 'credential-added',
43
+ CREDENTIAL_REMOVED = 'credential-removed',
44
+ CREDENTIAL_KIND_CHANGED = 'credential-kind-changed',
45
+ PERMISSION_TIGHTENED = 'permission-tightened',
46
+ PERMISSION_RELAXED = 'permission-relaxed',
47
+ PERSISTENCE_PATH_ADDED = 'persistence-path-added',
48
+ PERSISTENCE_PATH_REMOVED = 'persistence-path-removed',
49
+ OUTPUT_SURFACE_CHANGED = 'output-surface-changed',
50
+ ENV_VAR_ADDED = 'env-var-added',
51
+ ENV_VAR_REMOVED = 'env-var-removed',
52
+ ENV_VAR_VALUE_CHANGED = 'env-var-value-changed',
53
+ MOUNT_ADDED = 'mount-added',
54
+ MOUNT_REMOVED = 'mount-removed',
55
+ MOUNT_MODE_CHANGED = 'mount-mode-changed',
56
+ }
57
+
58
+ /**
59
+ * Advisory drift severity — `info` (benign) or `warning` (worth a look).
60
+ * There is deliberately no `critical`: resume is non-blocking by design
61
+ * (plan Epic D §D4), so severity only drives how the operator's drift
62
+ * toast renders, never whether the session resumes.
63
+ */
64
+ export type DriftSeverity = 'info' | 'warning';
65
+
66
+ export interface EnvironmentDrift {
67
+ readonly kind: DriftKind;
68
+ readonly severity: DriftSeverity;
69
+ readonly path: string;
70
+ readonly before?: unknown;
71
+ readonly after?: unknown;
72
+ readonly message: string;
73
+ }
74
+
75
+ /**
76
+ * Diff two `ResolvedEnvironment` snapshots. Returns a possibly-empty
77
+ * array of structured drifts. Order is stable (insertion order across
78
+ * the structured walk) so callers can render a deterministic timeline
79
+ * entry.
80
+ *
81
+ * Equal hashes short-circuit to `[]` — the hash covers every
82
+ * structured field, so two snapshots with the same hash are
83
+ * guaranteed to produce no drift.
84
+ */
85
+ export function diffResolvedEnvironment(
86
+ prev: ResolvedEnvironment,
87
+ next: ResolvedEnvironment,
88
+ ): readonly EnvironmentDrift[] {
89
+ if (prev.hash === next.hash) {return [];}
90
+
91
+ const drifts: EnvironmentDrift[] = [];
92
+
93
+ diffAgent(prev, next, drifts);
94
+ diffSubAgents(prev.subAgents, next.subAgents, drifts);
95
+ diffSkills(prev.skills, next.skills, drifts);
96
+ diffToolSelection(prev.toolSelection, next.toolSelection, drifts);
97
+ diffCredentials(prev.credentials, next.credentials, drifts);
98
+ diffPermissions(prev, next, drifts);
99
+ diffPersistence(prev, next, drifts);
100
+ diffOutputSurface(prev, next, drifts);
101
+ diffEnv(prev, next, drifts);
102
+ diffMounts(prev, next, drifts);
103
+
104
+ return drifts;
105
+ }
106
+
107
+ function diffAgent(
108
+ prev: ResolvedEnvironment,
109
+ next: ResolvedEnvironment,
110
+ out: EnvironmentDrift[],
111
+ ): void {
112
+ if (prev.manifest.version !== next.manifest.version) {
113
+ out.push({
114
+ kind: DriftKind.MANIFEST_VERSION_CHANGED,
115
+ severity: 'info',
116
+ path: '$.manifest.version',
117
+ before: prev.manifest.version,
118
+ after: next.manifest.version,
119
+ message: `Manifest version moved ${prev.manifest.version} → ${next.manifest.version}.`,
120
+ });
121
+ }
122
+ if (prev.agent.slug !== next.agent.slug) {
123
+ out.push({
124
+ kind: DriftKind.AGENT_CHANGED,
125
+ severity: 'warning',
126
+ path: '$.agent.slug',
127
+ before: prev.agent.slug,
128
+ after: next.agent.slug,
129
+ message: `Primary agent slug changed ${prev.agent.slug} → ${next.agent.slug}; the session resumes with the new agent.`,
130
+ });
131
+ }
132
+ if (prev.agent.role !== next.agent.role) {
133
+ out.push({
134
+ kind: DriftKind.ROLE_CHANGED,
135
+ severity: 'info',
136
+ path: '$.agent.role',
137
+ before: prev.agent.role,
138
+ after: next.agent.role,
139
+ message: `Agent role changed ${prev.agent.role} → ${next.agent.role}; AGENTS.md will be re-rendered.`,
140
+ });
141
+ }
142
+ const prevModel = JSON.stringify(prev.agent.model ?? null);
143
+ const nextModel = JSON.stringify(next.agent.model ?? null);
144
+ if (prevModel !== nextModel) {
145
+ out.push({
146
+ kind: DriftKind.MODEL_CHANGED,
147
+ severity: 'info',
148
+ path: '$.agent.model',
149
+ before: prev.agent.model,
150
+ after: next.agent.model,
151
+ message: 'Primary agent default model changed.',
152
+ });
153
+ }
154
+ }
155
+
156
+ function diffSubAgents(
157
+ prev: readonly SubAgentBinding[],
158
+ next: readonly SubAgentBinding[],
159
+ out: EnvironmentDrift[],
160
+ ): void {
161
+ const prevBySlug = new Map(prev.map((s) => [s.slug, s]));
162
+ const nextBySlug = new Map(next.map((s) => [s.slug, s]));
163
+ for (const [slug, n] of nextBySlug) {
164
+ const p = prevBySlug.get(slug);
165
+ if (!p) {
166
+ out.push({
167
+ kind: DriftKind.SUBAGENT_ADDED,
168
+ severity: 'info',
169
+ path: `$.subAgents[${slug}]`,
170
+ after: n,
171
+ message: `Sub-agent '${slug}' added.`,
172
+ });
173
+ continue;
174
+ }
175
+ if (JSON.stringify(p.modelOverride ?? null) !== JSON.stringify(n.modelOverride ?? null)) {
176
+ out.push({
177
+ kind: DriftKind.SUBAGENT_MODEL_CHANGED,
178
+ severity: 'info',
179
+ path: `$.subAgents[${slug}].modelOverride`,
180
+ before: p.modelOverride,
181
+ after: n.modelOverride,
182
+ message: `Sub-agent '${slug}' model override changed.`,
183
+ });
184
+ }
185
+ }
186
+ for (const [slug, p] of prevBySlug) {
187
+ if (!nextBySlug.has(slug)) {
188
+ out.push({
189
+ kind: DriftKind.SUBAGENT_REMOVED,
190
+ severity: 'warning',
191
+ path: `$.subAgents[${slug}]`,
192
+ before: p,
193
+ message: `Sub-agent '${slug}' removed; existing delegations may fail until the session is restarted.`,
194
+ });
195
+ }
196
+ }
197
+ }
198
+
199
+ function diffSkills(
200
+ prev: readonly CompiledManifestSkillRef[],
201
+ next: readonly CompiledManifestSkillRef[],
202
+ out: EnvironmentDrift[],
203
+ ): void {
204
+ const prevBySlug = new Map(prev.map((s) => [s.slug, s]));
205
+ const nextBySlug = new Map(next.map((s) => [s.slug, s]));
206
+ for (const [slug, n] of nextBySlug) {
207
+ const p = prevBySlug.get(slug);
208
+ if (!p) {
209
+ out.push({
210
+ kind: DriftKind.SKILL_ADDED,
211
+ severity: 'info',
212
+ path: `$.skills[${slug}]`,
213
+ after: n,
214
+ message: `Skill '${slug}' added.`,
215
+ });
216
+ continue;
217
+ }
218
+ if (p.version !== n.version) {
219
+ out.push({
220
+ kind: DriftKind.SKILL_VERSION_CHANGED,
221
+ severity: 'info',
222
+ path: `$.skills[${slug}].version`,
223
+ before: p.version,
224
+ after: n.version,
225
+ message: `Skill '${slug}' version moved ${p.version ?? '*'} → ${n.version ?? '*'}.`,
226
+ });
227
+ }
228
+ }
229
+ for (const [slug, p] of prevBySlug) {
230
+ if (!nextBySlug.has(slug)) {
231
+ out.push({
232
+ kind: DriftKind.SKILL_REMOVED,
233
+ severity: 'warning',
234
+ path: `$.skills[${slug}]`,
235
+ before: p,
236
+ message: `Skill '${slug}' removed.`,
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ function toolSelectionKey(entry: ToolSelectionEntry): string {
243
+ return entry.kind === 'tool'
244
+ ? `tool:${entry.providerKind}:${entry.resourceId}:${entry.toolName}`
245
+ : `provider:${entry.providerKind}:${entry.resourceId}`;
246
+ }
247
+
248
+ function diffToolSelection(
249
+ prev: readonly ToolSelectionEntry[],
250
+ next: readonly ToolSelectionEntry[],
251
+ out: EnvironmentDrift[],
252
+ ): void {
253
+ const prevByKey = new Map(prev.map((e) => [toolSelectionKey(e), e]));
254
+ const nextByKey = new Map(next.map((e) => [toolSelectionKey(e), e]));
255
+ for (const [key, n] of nextByKey) {
256
+ if (!prevByKey.has(key)) {
257
+ out.push({
258
+ kind: DriftKind.TOOL_SELECTION_ADDED,
259
+ severity: 'info',
260
+ path: `$.toolSelection[${key}]`,
261
+ after: n,
262
+ message: `Tool selection entry '${key}' added.`,
263
+ });
264
+ }
265
+ }
266
+ for (const [key, p] of prevByKey) {
267
+ if (!nextByKey.has(key)) {
268
+ out.push({
269
+ kind: DriftKind.TOOL_SELECTION_REMOVED,
270
+ severity: 'warning',
271
+ path: `$.toolSelection[${key}]`,
272
+ before: p,
273
+ message: `Tool selection entry '${key}' removed.`,
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ function diffCredentials(
280
+ prev: readonly CompiledManifestCredential[],
281
+ next: readonly CompiledManifestCredential[],
282
+ out: EnvironmentDrift[],
283
+ ): void {
284
+ const prevByName = new Map(prev.map((c) => [c.name, c]));
285
+ const nextByName = new Map(next.map((c) => [c.name, c]));
286
+ for (const [name, n] of nextByName) {
287
+ const p = prevByName.get(name);
288
+ if (!p) {
289
+ out.push({
290
+ kind: DriftKind.CREDENTIAL_ADDED,
291
+ severity: 'info',
292
+ path: `$.credentials[${name}]`,
293
+ after: n,
294
+ message: `Credential '${name}' added.`,
295
+ });
296
+ continue;
297
+ }
298
+ if (p.kind !== n.kind) {
299
+ out.push({
300
+ kind: DriftKind.CREDENTIAL_KIND_CHANGED,
301
+ severity: 'warning',
302
+ path: `$.credentials[${name}].kind`,
303
+ before: p.kind,
304
+ after: n.kind,
305
+ message: `Credential '${name}' kind changed ${p.kind} → ${n.kind}; the resolver will re-route it on resume.`,
306
+ });
307
+ }
308
+ }
309
+ for (const [name, p] of prevByName) {
310
+ if (!nextByName.has(name)) {
311
+ out.push({
312
+ kind: DriftKind.CREDENTIAL_REMOVED,
313
+ // Resume is non-blocking even for a previously-required
314
+ // credential — the restore path degrades to a still-usable
315
+ // provider. `warning` simply flags it for the operator.
316
+ severity: 'warning',
317
+ path: `$.credentials[${name}]`,
318
+ before: p,
319
+ message: `Credential '${name}' removed${p.required ? ' (was required; the session resumes with the remaining credentials)' : ''}.`,
320
+ });
321
+ }
322
+ }
323
+ }
324
+
325
+ function diffPermissions(
326
+ prev: ResolvedEnvironment,
327
+ next: ResolvedEnvironment,
328
+ out: EnvironmentDrift[],
329
+ ): void {
330
+ const prevAllow = new Set(prev.permissions.tools.allow);
331
+ const nextAllow = new Set(next.permissions.tools.allow);
332
+ const prevDeny = new Set(prev.permissions.tools.deny);
333
+ const nextDeny = new Set(next.permissions.tools.deny);
334
+ // A tool is "tightened" if it was previously allowed and is now
335
+ // explicitly denied OR removed from the allow-list.
336
+ for (const tool of prevAllow) {
337
+ if (!nextAllow.has(tool) || nextDeny.has(tool)) {
338
+ out.push({
339
+ kind: DriftKind.PERMISSION_TIGHTENED,
340
+ severity: 'warning',
341
+ path: `$.permissions.tools.${tool}`,
342
+ before: 'allow',
343
+ after: nextDeny.has(tool) ? 'deny' : 'unset',
344
+ message: `Tool '${tool}' is no longer allowed.`,
345
+ });
346
+ }
347
+ }
348
+ for (const tool of nextAllow) {
349
+ if (!prevAllow.has(tool) || prevDeny.has(tool)) {
350
+ out.push({
351
+ kind: DriftKind.PERMISSION_RELAXED,
352
+ severity: 'info',
353
+ path: `$.permissions.tools.${tool}`,
354
+ before: prevDeny.has(tool) ? 'deny' : 'unset',
355
+ after: 'allow',
356
+ message: `Tool '${tool}' is now allowed.`,
357
+ });
358
+ }
359
+ }
360
+ }
361
+
362
+ function diffPersistence(
363
+ prev: ResolvedEnvironment,
364
+ next: ResolvedEnvironment,
365
+ out: EnvironmentDrift[],
366
+ ): void {
367
+ const prevPaths = new Set(prev.persistence.paths);
368
+ const nextPaths = new Set(next.persistence.paths);
369
+ for (const path of nextPaths) {
370
+ if (!prevPaths.has(path)) {
371
+ out.push({
372
+ kind: DriftKind.PERSISTENCE_PATH_ADDED,
373
+ severity: 'info',
374
+ path: `$.persistence.paths`,
375
+ after: path,
376
+ message: `Persistence path '${path}' added; future pauses will snapshot it.`,
377
+ });
378
+ }
379
+ }
380
+ for (const path of prevPaths) {
381
+ if (!nextPaths.has(path)) {
382
+ out.push({
383
+ kind: DriftKind.PERSISTENCE_PATH_REMOVED,
384
+ severity: 'info',
385
+ path: `$.persistence.paths`,
386
+ before: path,
387
+ message: `Persistence path '${path}' removed; future pauses will not snapshot it.`,
388
+ });
389
+ }
390
+ }
391
+ }
392
+
393
+ function diffOutputSurface(
394
+ prev: ResolvedEnvironment,
395
+ next: ResolvedEnvironment,
396
+ out: EnvironmentDrift[],
397
+ ): void {
398
+ if (
399
+ prev.outputSurface.kind !== next.outputSurface.kind ||
400
+ prev.outputSurface.port !== next.outputSurface.port ||
401
+ prev.outputSurface.healthPath !== next.outputSurface.healthPath ||
402
+ prev.outputSurface.mode !== next.outputSurface.mode
403
+ ) {
404
+ out.push({
405
+ kind: DriftKind.OUTPUT_SURFACE_CHANGED,
406
+ severity: 'info',
407
+ path: '$.outputSurface',
408
+ before: prev.outputSurface,
409
+ after: next.outputSurface,
410
+ message: 'Output surface configuration changed.',
411
+ });
412
+ }
413
+ }
414
+
415
+ function diffEnv(
416
+ prev: ResolvedEnvironment,
417
+ next: ResolvedEnvironment,
418
+ out: EnvironmentDrift[],
419
+ ): void {
420
+ const prevByName = new Map(prev.env.map((e) => [e.name, e.value]));
421
+ const nextByName = new Map(next.env.map((e) => [e.name, e.value]));
422
+ for (const [name, value] of nextByName) {
423
+ const prevValue = prevByName.get(name);
424
+ if (prevValue === undefined) {
425
+ out.push({
426
+ kind: DriftKind.ENV_VAR_ADDED,
427
+ severity: 'info',
428
+ path: `$.env[${name}]`,
429
+ after: value,
430
+ message: `Env var '${name}' added.`,
431
+ });
432
+ } else if (prevValue !== value) {
433
+ out.push({
434
+ kind: DriftKind.ENV_VAR_VALUE_CHANGED,
435
+ severity: 'info',
436
+ path: `$.env[${name}]`,
437
+ before: prevValue,
438
+ after: value,
439
+ message: `Env var '${name}' value changed.`,
440
+ });
441
+ }
442
+ }
443
+ for (const name of prevByName.keys()) {
444
+ if (!nextByName.has(name)) {
445
+ out.push({
446
+ kind: DriftKind.ENV_VAR_REMOVED,
447
+ severity: 'warning',
448
+ path: `$.env[${name}]`,
449
+ before: prevByName.get(name),
450
+ message: `Env var '${name}' removed.`,
451
+ });
452
+ }
453
+ }
454
+ }
455
+
456
+ function diffMounts(
457
+ prev: ResolvedEnvironment,
458
+ next: ResolvedEnvironment,
459
+ out: EnvironmentDrift[],
460
+ ): void {
461
+ const prevSlots = new Map(prev.mountPlan.entries.map((e) => [e.slot, e]));
462
+ const nextSlots = new Map(next.mountPlan.entries.map((e) => [e.slot, e]));
463
+ for (const [slot, entry] of nextSlots) {
464
+ const prevEntry = prevSlots.get(slot);
465
+ if (!prevEntry) {
466
+ out.push({
467
+ kind: DriftKind.MOUNT_ADDED,
468
+ severity: 'info',
469
+ path: `$.mounts[${slot}]`,
470
+ after: { mountKey: entry.mountKey },
471
+ message: `Mount slot '${slot}' added.`,
472
+ });
473
+ continue;
474
+ }
475
+ if (prevEntry.mode !== entry.mode) {
476
+ out.push({
477
+ kind: DriftKind.MOUNT_MODE_CHANGED,
478
+ severity: 'warning',
479
+ path: `$.mounts[${slot}].mode`,
480
+ before: prevEntry.mode,
481
+ after: entry.mode,
482
+ message: `Mount slot '${slot}' mode changed ${prevEntry.mode} → ${entry.mode}.`,
483
+ });
484
+ }
485
+ }
486
+ for (const slot of prevSlots.keys()) {
487
+ if (!nextSlots.has(slot)) {
488
+ out.push({
489
+ kind: DriftKind.MOUNT_REMOVED,
490
+ severity: 'warning',
491
+ path: `$.mounts[${slot}]`,
492
+ before: { slot },
493
+ message: `Mount slot '${slot}' removed.`,
494
+ });
495
+ }
496
+ }
497
+ }