@zq-silk/yui 0.6.2 → 0.6.4

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 (63) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +60 -97
  3. package/dist/agent/argumentPolicy.js +1 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +1 -0
  5. package/dist/cli/commandCatalog.js +19 -9
  6. package/dist/cli/interactionPolicy.js +4 -2
  7. package/dist/cli.js +77 -32
  8. package/dist/commands/taskCommands.js +46 -11
  9. package/dist/commands/taskContextCommand.js +1 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +170 -10
  11. package/dist/controller/agentRuntimeObserver.js +210 -0
  12. package/dist/controller/clientRuntime.js +3 -21
  13. package/dist/controller/controller.js +47 -7
  14. package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
  15. package/dist/controller/runtime.js +9 -3
  16. package/dist/controller/runtimeEventInbox.js +49 -295
  17. package/dist/controller/runtimeEventProcessor.js +184 -321
  18. package/dist/controller/runtimeHookRunFence.js +226 -0
  19. package/dist/controller/runtimeLaunchCoordinator.js +91 -26
  20. package/dist/controller/runtimeObservationHook.js +112 -0
  21. package/dist/core/controllerServer.js +5 -0
  22. package/dist/executor/agentAdapter.js +18 -3
  23. package/dist/executor/fileRoleLaunchPlanner.js +64 -15
  24. package/dist/executor/managedClaudeRunner.js +121 -0
  25. package/dist/observability/executionAudit.js +6 -3
  26. package/dist/repository/taskWorkspacePreparer.js +1 -4
  27. package/dist/run/providerRetryConfig.js +8 -3
  28. package/dist/runtime/agentDriver.js +229 -0
  29. package/dist/runtime/agentDriverObservation.js +57 -0
  30. package/dist/runtime/builtinAgentDrivers.js +235 -0
  31. package/dist/runtime/builtinTranscriptObserver.js +290 -0
  32. package/dist/runtime/builtinTranscriptUsage.js +97 -0
  33. package/dist/runtime/exactControlPlane.js +2 -2
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/ports.js +12 -1
  36. package/dist/runtime/runtimeObservation.js +297 -0
  37. package/dist/runtime/runtimeProjection.js +277 -0
  38. package/dist/runtime/sessionTerminationGuard.js +78 -22
  39. package/dist/runtime/tmuxAdapters.js +35 -0
  40. package/dist/scheduler/activeRoleRunDelivery.js +28 -13
  41. package/dist/scheduler/leaderWakeupProcessor.js +21 -2
  42. package/dist/scheduler/roleRunLiveness.js +2 -2
  43. package/dist/scheduler/roleRunStall.js +62 -114
  44. package/dist/storage/migration/productionRegistry.js +85 -0
  45. package/dist/storage/sqliteStore.js +3 -3
  46. package/dist/storage/storageVersions.js +1 -1
  47. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
  48. package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
  49. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  50. package/dist/telemetry/telemetryCompaction.js +1 -0
  51. package/dist/telemetry/telemetryConfig.js +4 -5
  52. package/dist/tmux/tmuxManager.js +136 -22
  53. package/dist/web/assets/client/view.js +1 -1
  54. package/dist/web/tmuxWebTerminal.js +17 -12
  55. package/dist/web/webSnapshot.js +1 -1
  56. package/dist/worktree/managedWorkspace.js +14 -0
  57. package/i18n/README.zh-CN.md +12 -7
  58. package/package.json +1 -1
  59. package/dist/controller/claudeLifecycleHook.js +0 -203
  60. package/dist/controller/codexLifecycleHook.js +0 -108
  61. package/dist/controller/providerHookRunFence.js +0 -156
  62. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  63. package/dist/telemetry/telemetryRouter.js +0 -32
@@ -19,6 +19,8 @@ import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactT
19
19
  import { readActiveReleasePointer } from "../release/runtimeRelease.js";
20
20
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
21
21
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
22
+ import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
23
+ import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
22
24
  /** Builds managed native Agent launches from the authoritative Task records. */
23
25
  export class FileRoleLaunchPlanner {
24
26
  home;
@@ -304,14 +306,24 @@ export class FileRoleLaunchPlanner {
304
306
  trustWorkspace: true
305
307
  })
306
308
  : undefined;
307
- if (codexConfig?.notify.status === "configured") {
309
+ if (codexConfig?.notify.status === "configured"
310
+ && (owner.scope !== "task" || input.runId === undefined)) {
308
311
  throw new Error("Codex notify is already configured by "
309
- + `${codexConfig.notify.source}; Yui requires exclusive ownership of the structured `
310
- + "notify callback and refuses to replace or be replaced by native configuration.");
312
+ + `${codexConfig.notify.source}; this interactive Yui Session requires exclusive `
313
+ + "ownership of the structured notify callback and refuses to replace or be replaced "
314
+ + "by native configuration.");
311
315
  }
312
316
  const managedRun = owner.scope === "task" && input.runId !== undefined
313
317
  ? this.store.getAgentRun(owner.taskId, input.runId)
314
318
  : null;
319
+ const driver = builtinAgentDriverRegistry().require(builtinDriverIdForAdapter(configured.adapterId));
320
+ if (owner.scope === "task" && input.runId !== undefined) {
321
+ const admission = managedRuntimeAdmission(driver.capabilities);
322
+ if (!admission.admitted) {
323
+ throw new Error(`Agent Driver ${driver.id} cannot host managed Runs; missing capabilities: `
324
+ + admission.missing.join(", "));
325
+ }
326
+ }
315
327
  const roleConfig = binding.config.adapterId === "claude"
316
328
  && owner.scope === "task"
317
329
  && input.runId !== undefined
@@ -352,9 +364,15 @@ export class FileRoleLaunchPlanner {
352
364
  this.#resourceRegistrar().registerSessionContext(sessionContext.managedContextFile, { home: resolve(this.home), taskId: owner.taskId, basis: "descriptor" });
353
365
  }
354
366
  let args = [...compiled.argv];
367
+ let command = configured.command;
355
368
  let session;
356
369
  if (binding.adapterId === "codex") {
357
- args = addCodexSessionNotify(args, launchMode, this.#cliPath);
370
+ // Global/interactive Codex sessions still use notify for presentation.
371
+ // Managed Runs use the structured Driver Hook as their sole lifecycle
372
+ // authority, avoiding two terminal channels for the same Turn.
373
+ if (owner.scope !== "task" || input.runId === undefined) {
374
+ args = addCodexSessionNotify(args, launchMode, this.#cliPath);
375
+ }
358
376
  // A fresh Codex TUI has no provider event before its first prompt. Carry
359
377
  // the exact Run input as the provider's positional launch prompt so it is
360
378
  // submitted only after Codex completes startup; never race terminal bytes
@@ -374,6 +392,7 @@ export class FileRoleLaunchPlanner {
374
392
  }
375
393
  else if (launchMode === "new") {
376
394
  if (owner.scope === "task" && input.runId !== undefined) {
395
+ args.push("-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose");
377
396
  args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
378
397
  }
379
398
  const nativeSessionId = requireText(input.launchId === undefined
@@ -384,10 +403,25 @@ export class FileRoleLaunchPlanner {
384
403
  }
385
404
  else {
386
405
  if (owner.scope === "task" && input.runId !== undefined) {
406
+ args.push("-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose");
387
407
  args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
388
408
  }
389
409
  session = readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective);
390
410
  }
411
+ const managedClaudeRun = binding.adapterId === "claude"
412
+ && owner.scope === "task"
413
+ && input.runId !== undefined;
414
+ if (managedClaudeRun) {
415
+ command = process.execPath;
416
+ args = [
417
+ this.#cliPath,
418
+ "internal",
419
+ "managed-claude-run",
420
+ "--",
421
+ configured.command,
422
+ ...args
423
+ ];
424
+ }
391
425
  const runtimeDescriptor = owner.scope === "task"
392
426
  ? createExactTaskRuntimeDescriptor({
393
427
  controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
@@ -418,7 +452,7 @@ export class FileRoleLaunchPlanner {
418
452
  jobCallerKey = randomBytes(32).toString("hex");
419
453
  }
420
454
  const launch = {
421
- command: configured.command,
455
+ command,
422
456
  args,
423
457
  env: {
424
458
  ...launchEnvironment,
@@ -428,6 +462,7 @@ export class FileRoleLaunchPlanner {
428
462
  YUI_ROLE: role.name,
429
463
  YUI_AGENT_ID: configured.id,
430
464
  YUI_ADAPTER_ID: configured.adapterId,
465
+ YUI_DRIVER_ID: driver.id,
431
466
  YUI_WORKSPACE: effectiveWorkspace,
432
467
  ...(jobCallerKey === undefined ? {} : { YUI_JOB_CALLER_KEY: jobCallerKey }),
433
468
  ...(runtimeDescriptor === undefined
@@ -468,7 +503,7 @@ export class FileRoleLaunchPlanner {
468
503
  },
469
504
  launch: scopedLaunch,
470
505
  session,
471
- ...(binding.adapterId === "codex" && input.runId !== undefined
506
+ ...((binding.adapterId === "codex" || managedClaudeRun) && input.runId !== undefined
472
507
  ? { initialPromptRunId: input.runId }
473
508
  : {})
474
509
  };
@@ -570,19 +605,24 @@ function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
570
605
  const command = {
571
606
  type: "command",
572
607
  command: canonicalPath(process.execPath),
573
- args: [canonicalPath(cliPath), "internal", "claude-hook"]
608
+ args: [canonicalPath(cliPath), "internal", "runtime-hook"]
574
609
  };
575
610
  writeTextFileAtomically(join(root, "hooks", "hooks.json"), `${JSON.stringify({
576
611
  hooks: {
577
- // SessionStart proves the session exists (and, for source=startup,
578
- // pre-input readiness); UserPromptSubmit is the exact provider-accepted
579
- // fence; PostToolUse is a provider-native in-turn progress fact and
580
- // StopFailure is the terminal failure fact. All route to the same
581
- // Yui-owned entrypoint, which parses by hook_event_name.
612
+ // All native names terminate at the Driver edge; core sees only
613
+ // canonical RuntimeObservation values.
582
614
  SessionStart: [{ hooks: [command] }],
583
615
  UserPromptSubmit: [{ hooks: [command] }],
616
+ PreToolUse: [{ hooks: [command] }],
617
+ PermissionRequest: [{ hooks: [command] }],
618
+ MessageDisplay: [{ hooks: [command] }],
584
619
  PostToolUse: [{ hooks: [command] }],
585
- StopFailure: [{ hooks: [command] }]
620
+ PostToolUseFailure: [{ hooks: [command] }],
621
+ SubagentStart: [{ hooks: [command] }],
622
+ SubagentStop: [{ hooks: [command] }],
623
+ Stop: [{ hooks: [command] }],
624
+ StopFailure: [{ hooks: [command] }],
625
+ SessionEnd: [{ hooks: [command] }]
586
626
  }
587
627
  }, null, 2)}\n`);
588
628
  return root;
@@ -669,10 +709,19 @@ function codexLifecycleHooksConfig(cliPath) {
669
709
  shellQuote(canonicalPath(process.execPath)),
670
710
  shellQuote(canonicalPath(cliPath)),
671
711
  "internal",
672
- "codex-hook"
712
+ "runtime-hook"
673
713
  ].join(" ");
674
714
  const handler = `{hooks=[{type="command",command=${JSON.stringify(command)}}]}`;
675
- return `hooks={SessionStart=[${handler}],UserPromptSubmit=[${handler}]}`;
715
+ return `hooks={`
716
+ + `SessionStart=[${handler}],`
717
+ + `UserPromptSubmit=[${handler}],`
718
+ + `PreToolUse=[${handler}],`
719
+ + `PermissionRequest=[${handler}],`
720
+ + `PostToolUse=[${handler}],`
721
+ + `SubagentStart=[${handler}],`
722
+ + `SubagentStop=[${handler}],`
723
+ + `Stop=[${handler}]`
724
+ + `}`;
676
725
  }
677
726
  function addCodexLifecycleHooks(args, mode, cliPath) {
678
727
  // Session flags are Yui-owned and exact to this launch. Hook trust bypass is
@@ -0,0 +1,121 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:os";
3
+ const SIGKILL_GRACE_MS = 2_000;
4
+ /** The documented Claude Code stream-json user-message envelope. */
5
+ export function buildManagedClaudeInput(prompt) {
6
+ if (typeof prompt !== "string" || prompt.includes("\0")) {
7
+ throw new TypeError("Managed Claude prompt must be text without NUL bytes.");
8
+ }
9
+ return `${JSON.stringify({
10
+ type: "user",
11
+ message: {
12
+ role: "user",
13
+ content: [{ type: "text", text: prompt }]
14
+ }
15
+ })}\n`;
16
+ }
17
+ /**
18
+ * Runs one autonomous Claude turn without exposing the prompt in argv or
19
+ * depending on terminal readiness/key timing. stdout/stderr are connected
20
+ * before stdin is written, so a provider startup burst cannot deadlock the
21
+ * prompt pipe. EOF makes this a finite per-Run process; native continuity is
22
+ * carried by Claude's --session-id/--resume arguments.
23
+ */
24
+ export async function runManagedClaudeProcess(input) {
25
+ const command = requireProcessToken(input.command, "Managed Claude command");
26
+ const args = input.args.map((value) => requireProcessToken(value, "Managed Claude argument"));
27
+ const child = spawn(command, args, {
28
+ ...(input.cwd === undefined ? {} : { cwd: input.cwd }),
29
+ env: input.environment ?? process.env,
30
+ stdio: [
31
+ "pipe",
32
+ input.stdout === undefined ? "inherit" : "pipe",
33
+ input.stderr === undefined ? "inherit" : "pipe"
34
+ ],
35
+ // Linux-only package: isolate the Provider process tree so tmux/session
36
+ // cancellation cannot leave Claude tool descendants behind.
37
+ detached: true
38
+ });
39
+ if (child.stdin === null) {
40
+ child.kill();
41
+ throw new Error("Managed Claude stdin pipe is unavailable.");
42
+ }
43
+ if (input.stdout !== undefined)
44
+ child.stdout.pipe(input.stdout, { end: false });
45
+ if (input.stderr !== undefined)
46
+ child.stderr.pipe(input.stderr, { end: false });
47
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
48
+ const handlers = new Map();
49
+ let killTimer;
50
+ let terminationRequested = false;
51
+ let childSettled = false;
52
+ const signalProcessGroup = (signal) => {
53
+ if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
54
+ return;
55
+ try {
56
+ process.kill(-child.pid, signal);
57
+ }
58
+ catch (error) {
59
+ if (error.code !== "ESRCH")
60
+ throw error;
61
+ }
62
+ };
63
+ const terminateProcessGroup = (signal) => {
64
+ if (terminationRequested)
65
+ return;
66
+ terminationRequested = true;
67
+ signalProcessGroup(signal);
68
+ killTimer = setTimeout(() => signalProcessGroup("SIGKILL"), SIGKILL_GRACE_MS);
69
+ killTimer.unref();
70
+ };
71
+ for (const signal of signals) {
72
+ const handler = () => terminateProcessGroup(signal);
73
+ handlers.set(signal, handler);
74
+ process.on(signal, handler);
75
+ }
76
+ try {
77
+ const closed = new Promise((resolve, reject) => {
78
+ child.once("error", (error) => {
79
+ childSettled = true;
80
+ reject(error);
81
+ });
82
+ child.once("close", (code, signal) => {
83
+ childSettled = true;
84
+ resolve({ code, signal });
85
+ });
86
+ });
87
+ try {
88
+ await new Promise((resolve, reject) => {
89
+ child.stdin.once("error", reject);
90
+ child.stdin.end(buildManagedClaudeInput(input.prompt), "utf8", resolve);
91
+ });
92
+ }
93
+ catch (error) {
94
+ // A failed prompt pipe is not permission to orphan a Provider process or
95
+ // its tools. Terminate the whole group and wait for bounded convergence.
96
+ if (!childSettled) {
97
+ terminateProcessGroup("SIGTERM");
98
+ await closed.catch(() => undefined);
99
+ }
100
+ throw error;
101
+ }
102
+ const result = await closed;
103
+ return result.code ?? (result.signal === null ? 1 : signalExitCode(result.signal));
104
+ }
105
+ finally {
106
+ if (killTimer !== undefined)
107
+ clearTimeout(killTimer);
108
+ for (const [signal, handler] of handlers)
109
+ process.removeListener(signal, handler);
110
+ }
111
+ }
112
+ function requireProcessToken(value, label) {
113
+ if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
114
+ throw new TypeError(`${label} must be non-empty text without NUL bytes.`);
115
+ }
116
+ return value;
117
+ }
118
+ function signalExitCode(signal) {
119
+ const number = constants.signals[signal];
120
+ return number === undefined ? 1 : 128 + number;
121
+ }
@@ -14,6 +14,7 @@ import { readdirSync, statSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
16
16
  import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
17
+ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
17
18
  import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
18
19
  import { UNSUPPORTED } from "./runtimeIdentity.js";
19
20
  export function createProductionExecutionAuditPorts() {
@@ -289,9 +290,10 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
289
290
  continue;
290
291
  if (event.type === "runtime.role-session-reset")
291
292
  resets += 1;
292
- else if (event.type === "runtime.provider-session-lifecycle")
293
+ else if (runtimeObservationFromTaskEvent(event)?.kind.startsWith("session.")) {
293
294
  lifecycleEvents += 1;
294
- else if (event.type === "runtime.claude-stop-failure")
295
+ }
296
+ else if (event.type === "runtime.turn-failed")
295
297
  stopFailures += 1;
296
298
  }
297
299
  }
@@ -420,8 +422,9 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
420
422
  if (!inWindow(event.createdAt, options))
421
423
  continue;
422
424
  total += 1;
423
- if (event.type === "runtime.provider-turn-progress")
425
+ if (runtimeObservationFromTaskEvent(event)?.kind === "activity.observed") {
424
426
  progressEvents += 1;
427
+ }
425
428
  else if (event.type === "runtime.event-obsolete")
426
429
  obsoleteEvents += 1;
427
430
  }
@@ -8,7 +8,7 @@ import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "..
8
8
  import { StorageConflictError } from "../storage/taskStore.js";
9
9
  import { bindTaskWorkspaceIdentity } from "../task/task.js";
10
10
  import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
11
- import { createManagedWorkspace, managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
11
+ import { createManagedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
12
12
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
13
13
  import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
14
14
  import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
@@ -2542,9 +2542,6 @@ function sameEffectiveWorkspace(effective, workspace) {
2542
2542
  function sameManagedWorkspace(left, right) {
2543
2543
  return isDeepStrictEqual(left, right);
2544
2544
  }
2545
- function sameManagedWorkspaceIdentity(left, right) {
2546
- return isDeepStrictEqual({ owner: left.owner, root: left.root, entries: left.entries }, { owner: right.owner, root: right.root, entries: right.entries });
2547
- }
2548
2545
  function isMissingPath(error) {
2549
2546
  return typeof error === "object" && error !== null && "code" in error
2550
2547
  && error.code === "ENOENT";
@@ -1,20 +1,25 @@
1
- const KNOWN_ADAPTERS = ["claude", "codex"];
1
+ import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
2
2
  function parseAdapters(value) {
3
3
  if (value === undefined)
4
4
  return [];
5
+ const supported = supportedAgentAdapterIds();
6
+ const supportedSet = new Set(supported);
5
7
  const adapters = [];
6
8
  for (const raw of value.split(",")) {
7
9
  const token = raw.trim().toLowerCase();
8
10
  if (token === "")
9
11
  continue;
10
12
  if (token === "all") {
11
- for (const adapter of KNOWN_ADAPTERS) {
13
+ for (const adapter of supported) {
12
14
  if (!adapters.includes(adapter))
13
15
  adapters.push(adapter);
14
16
  }
15
17
  continue;
16
18
  }
17
- if (!KNOWN_ADAPTERS.includes(token)) {
19
+ if (!/^[a-z0-9][a-z0-9._-]*$/u.test(token)) {
20
+ throw new Error(`Invalid Provider retry adapter: ${token}.`);
21
+ }
22
+ if (!supportedSet.has(token)) {
18
23
  throw new Error(`Unknown Provider retry adapter: ${token}.`);
19
24
  }
20
25
  if (!adapters.includes(token)) {
@@ -0,0 +1,229 @@
1
+ const SURFACES = ["managed-protocol", "interactive-cli"];
2
+ const OPERATIONS = ["model", "tool", "subagent"];
3
+ const WAIT_REASONS = ["user", "permission", "external"];
4
+ const USAGE_MODES = [
5
+ "streaming-cumulative",
6
+ "event-snapshot",
7
+ "terminal-cumulative",
8
+ "unavailable"
9
+ ];
10
+ const DELIVERY_MODES = [
11
+ "ordered-replayable",
12
+ "ordered-best-effort",
13
+ "best-effort",
14
+ "host-only"
15
+ ];
16
+ export function validateAgentDriverCapabilities(input) {
17
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
18
+ throw new Error("Agent Driver capabilities must be an object.");
19
+ }
20
+ const surfaces = uniqueMembers(input.surfaces, SURFACES, "Agent Driver surface");
21
+ if (surfaces.length === 0)
22
+ throw new Error("Agent Driver must expose at least one surface.");
23
+ const control = input.control;
24
+ if (control === null || typeof control !== "object" || Array.isArray(control)) {
25
+ throw new Error("Agent Driver control capabilities must be an object.");
26
+ }
27
+ for (const name of ["start", "resume", "sendTurn", "interrupt", "stop"]) {
28
+ if (typeof control[name] !== "boolean") {
29
+ throw new Error(`Agent Driver control capability ${name} must be boolean.`);
30
+ }
31
+ }
32
+ const observation = input.observation;
33
+ if (observation === null || typeof observation !== "object" || Array.isArray(observation)) {
34
+ throw new Error("Agent Driver observation capabilities must be an object.");
35
+ }
36
+ if (observation.sessionIdentity !== "exact" && observation.sessionIdentity !== "unavailable") {
37
+ throw new Error("Agent Driver session identity capability is invalid.");
38
+ }
39
+ if (observation.sessionBootstrap !== "preallocated"
40
+ && observation.sessionBootstrap !== "discovered") {
41
+ throw new Error("Agent Driver Session bootstrap capability is invalid.");
42
+ }
43
+ if (observation.preInputReadiness !== "exact"
44
+ && observation.preInputReadiness !== "unavailable") {
45
+ throw new Error("Agent Driver pre-input readiness capability is invalid.");
46
+ }
47
+ if (observation.promptAcceptance !== "exact" && observation.promptAcceptance !== "unavailable") {
48
+ throw new Error("Agent Driver prompt acceptance capability is invalid.");
49
+ }
50
+ if (!["exact", "partial", "unavailable"].includes(observation.turnLifecycle)) {
51
+ throw new Error("Agent Driver turn lifecycle capability is invalid.");
52
+ }
53
+ if (!USAGE_MODES.includes(observation.usage)) {
54
+ throw new Error("Agent Driver usage capability is invalid.");
55
+ }
56
+ if (!DELIVERY_MODES.includes(observation.delivery)) {
57
+ throw new Error("Agent Driver delivery capability is invalid.");
58
+ }
59
+ return Object.freeze({
60
+ surfaces: Object.freeze(surfaces),
61
+ control: Object.freeze({
62
+ start: control.start,
63
+ resume: control.resume,
64
+ sendTurn: control.sendTurn,
65
+ interrupt: control.interrupt,
66
+ stop: control.stop
67
+ }),
68
+ observation: Object.freeze({
69
+ sessionIdentity: observation.sessionIdentity,
70
+ sessionBootstrap: observation.sessionBootstrap,
71
+ preInputReadiness: observation.preInputReadiness,
72
+ promptAcceptance: observation.promptAcceptance,
73
+ turnLifecycle: observation.turnLifecycle,
74
+ operations: Object.freeze(uniqueMembers(observation.operations, OPERATIONS, "Agent Driver operation")),
75
+ waiting: Object.freeze(uniqueMembers(observation.waiting, WAIT_REASONS, "Agent Driver wait reason")),
76
+ usage: observation.usage,
77
+ delivery: observation.delivery
78
+ })
79
+ });
80
+ }
81
+ export function managedRuntimeAdmission(capabilities) {
82
+ const actual = validateAgentDriverCapabilities(capabilities);
83
+ const missing = [];
84
+ if (!actual.control.start)
85
+ missing.push("start");
86
+ if (!actual.control.resume)
87
+ missing.push("resume");
88
+ if (!actual.control.sendTurn)
89
+ missing.push("send-turn");
90
+ if (!actual.control.interrupt)
91
+ missing.push("interrupt");
92
+ if (!actual.control.stop)
93
+ missing.push("stop");
94
+ if (actual.observation.sessionIdentity !== "exact")
95
+ missing.push("exact-session-identity");
96
+ if (actual.observation.promptAcceptance !== "exact")
97
+ missing.push("exact-prompt-acceptance");
98
+ if (actual.observation.turnLifecycle !== "exact")
99
+ missing.push("exact-turn-lifecycle");
100
+ return missing.length === 0
101
+ ? Object.freeze({ admitted: true })
102
+ : Object.freeze({ admitted: false, missing: Object.freeze(missing) });
103
+ }
104
+ export class AgentDriverRegistry {
105
+ #drivers = new Map();
106
+ #driverIdsByAdapter = new Map();
107
+ register(input) {
108
+ const id = requireDriverId(input.id);
109
+ if (this.#drivers.has(id))
110
+ throw new Error(`Agent Driver is already registered: ${id}.`);
111
+ const adapterId = requireAdapterId(input.adapterId);
112
+ const existingDriverId = this.#driverIdsByAdapter.get(adapterId);
113
+ if (existingDriverId !== undefined) {
114
+ throw new Error(`Agent adapter ${adapterId} is already owned by Driver ${existingDriverId}.`);
115
+ }
116
+ if (input.protocolVersion !== 1) {
117
+ throw new Error(`Agent Driver protocol version is unsupported: ${String(input.protocolVersion)}.`);
118
+ }
119
+ if (input.runtime === null || typeof input.runtime !== "object") {
120
+ throw new Error("Agent Driver runtime must be an object.");
121
+ }
122
+ if (typeof input.runtime.nativeSessionId !== "function"
123
+ || typeof input.runtime.nativeTurnId !== "function"
124
+ || typeof input.runtime.mapHook !== "function"
125
+ || typeof input.runtime.classifyHook !== "function"
126
+ || (input.runtime.observer !== undefined
127
+ && (typeof input.runtime.observer.source !== "function"
128
+ || typeof input.runtime.observer.sample !== "function"))) {
129
+ throw new Error("Agent Driver runtime observation functions are invalid.");
130
+ }
131
+ const driver = Object.freeze({
132
+ id,
133
+ label: requireText(input.label, "Agent Driver label"),
134
+ protocolVersion: 1,
135
+ adapterId,
136
+ capabilities: validateAgentDriverCapabilities(input.capabilities),
137
+ runtime: Object.freeze({
138
+ nativeSessionId: input.runtime.nativeSessionId,
139
+ nativeTurnId: input.runtime.nativeTurnId,
140
+ mapHook: input.runtime.mapHook,
141
+ classifyHook: input.runtime.classifyHook,
142
+ ...(input.runtime.observer === undefined
143
+ ? {}
144
+ : { observer: Object.freeze({
145
+ source: input.runtime.observer.source,
146
+ sample: input.runtime.observer.sample
147
+ }) })
148
+ })
149
+ });
150
+ this.#drivers.set(id, driver);
151
+ this.#driverIdsByAdapter.set(adapterId, id);
152
+ return driver;
153
+ }
154
+ find(id) {
155
+ return this.#drivers.get(id) ?? null;
156
+ }
157
+ require(id) {
158
+ const driver = this.find(id);
159
+ if (driver === null)
160
+ throw new Error(`Agent Driver is not registered: ${id}.`);
161
+ return driver;
162
+ }
163
+ findByAdapterId(adapterId) {
164
+ const driverId = this.#driverIdsByAdapter.get(adapterId);
165
+ return driverId === undefined ? null : this.require(driverId);
166
+ }
167
+ requireByAdapterId(adapterId) {
168
+ const driver = this.findByAdapterId(adapterId);
169
+ if (driver === null) {
170
+ throw new Error(`Agent Driver is not registered for adapter: ${adapterId}.`);
171
+ }
172
+ return driver;
173
+ }
174
+ list() {
175
+ return [...this.#drivers.values()].sort((left, right) => left.id.localeCompare(right.id));
176
+ }
177
+ }
178
+ export function requireDriverId(value) {
179
+ const id = requireText(value, "Agent Driver id");
180
+ if (!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/u.test(id)) {
181
+ throw new Error("Agent Driver id must be a lowercase namespaced identity.");
182
+ }
183
+ return id;
184
+ }
185
+ export function normalizeAgentDriverHookClassification(input) {
186
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
187
+ throw new Error("Agent Driver Hook classification must be an object.");
188
+ }
189
+ if (input.startupSession !== undefined
190
+ && input.startupSession !== "preallocated"
191
+ && input.startupSession !== "discovered") {
192
+ throw new Error("Agent Driver Hook startup Session classification is invalid.");
193
+ }
194
+ if (input.terminal !== undefined && typeof input.terminal !== "boolean") {
195
+ throw new Error("Agent Driver Hook terminal classification is invalid.");
196
+ }
197
+ return Object.freeze({
198
+ ...(input.startupSession === undefined ? {} : { startupSession: input.startupSession }),
199
+ terminal: input.terminal ?? false
200
+ });
201
+ }
202
+ function uniqueMembers(input, supported, label) {
203
+ if (!Array.isArray(input))
204
+ throw new Error(`${label}s must be an array.`);
205
+ const result = [];
206
+ for (const value of input) {
207
+ if (!supported.includes(value))
208
+ throw new Error(`${label} is invalid: ${String(value)}.`);
209
+ if (!result.includes(value))
210
+ result.push(value);
211
+ }
212
+ return result;
213
+ }
214
+ function requireText(value, label) {
215
+ if (typeof value !== "string" || value.includes("\0"))
216
+ throw new Error(`${label} is invalid.`);
217
+ const normalized = value.trim();
218
+ if (normalized.length === 0 || normalized.length > 1_024) {
219
+ throw new Error(`${label} is invalid.`);
220
+ }
221
+ return normalized;
222
+ }
223
+ function requireAdapterId(value) {
224
+ const id = requireText(value, "Agent adapter id");
225
+ if (!/^[a-z0-9][a-z0-9._-]*$/u.test(id)) {
226
+ throw new Error("Agent adapter id must be a lowercase identity.");
227
+ }
228
+ return id;
229
+ }
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createRuntimeObservation } from "./runtimeObservation.js";
3
+ export function mapAgentDriverHook(input) {
4
+ requireMatchingDriver(input);
5
+ const mapped = input.driver.runtime.mapHook({
6
+ hookEventName: input.hookEventName,
7
+ payload: input.payload,
8
+ ...(input.occurrenceId === undefined ? {} : { occurrenceId: input.occurrenceId })
9
+ });
10
+ const source = mapped.kind === "turn.accepted"
11
+ ? input.driver.runtime.observer?.source({
12
+ hookEventName: input.hookEventName,
13
+ payload: input.payload,
14
+ ...(input.occurrenceId === undefined ? {} : { occurrenceId: input.occurrenceId })
15
+ }) ?? null
16
+ : null;
17
+ return observation(input, source === null ? mapped : {
18
+ ...mapped,
19
+ payload: Object.freeze({ ...mapped.payload, observerSource: source })
20
+ });
21
+ }
22
+ export function mapAgentDriverUsage(input, usage) {
23
+ requireMatchingDriver(input);
24
+ return observation(input, {
25
+ kind: "activity.observed",
26
+ payload: Object.freeze({ activity: "model", usage })
27
+ });
28
+ }
29
+ function observation(input, mapped) {
30
+ return createRuntimeObservation({
31
+ schemaVersion: 1,
32
+ eventId: hookEventId(input, mapped),
33
+ kind: mapped.kind,
34
+ authority: "provider-structured",
35
+ receivedAt: input.receivedAt,
36
+ ...(input.observedAt === undefined ? {} : { observedAt: input.observedAt }),
37
+ ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
38
+ ...(input.ordinal === undefined ? {} : { ordinal: input.ordinal }),
39
+ fence: input.fence,
40
+ payload: mapped.payload
41
+ });
42
+ }
43
+ function requireMatchingDriver(input) {
44
+ if (input.fence.driverId !== input.driver.id) {
45
+ throw new Error("Agent Driver Hook fence does not match the selected Driver.");
46
+ }
47
+ }
48
+ function hookEventId(input, mapped) {
49
+ const digest = createHash("sha256").update(JSON.stringify([
50
+ input.driver.id,
51
+ input.hookEventName,
52
+ input.fence,
53
+ mapped.kind,
54
+ mapped.payload
55
+ ])).digest("hex");
56
+ return `runtime-observation-${digest}`;
57
+ }