@xenosystem/agent-interface-host 0.1.4 → 0.1.6

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 (124) hide show
  1. package/dist/acp/AcpAgentRegistryService.d.ts +43 -1
  2. package/dist/acp/AcpAgentRegistryService.d.ts.map +1 -1
  3. package/dist/acp/AcpAgentRegistryService.js +200 -1
  4. package/dist/acp/AcpAgentRegistryService.js.map +1 -1
  5. package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts +66 -0
  6. package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts.map +1 -1
  7. package/dist/acp/AcpAgentTurnExecutionAdapter.js +182 -1
  8. package/dist/acp/AcpAgentTurnExecutionAdapter.js.map +1 -1
  9. package/dist/acp/AcpPermissionBridge.d.ts.map +1 -1
  10. package/dist/acp/AcpPermissionBridge.js +3 -3
  11. package/dist/acp/AcpPermissionBridge.js.map +1 -1
  12. package/dist/acp/AcpProcessSupervisor.d.ts +81 -0
  13. package/dist/acp/AcpProcessSupervisor.d.ts.map +1 -1
  14. package/dist/acp/AcpProcessSupervisor.js +176 -2
  15. package/dist/acp/AcpProcessSupervisor.js.map +1 -1
  16. package/dist/acp/AcpProviderCatalogService.d.ts.map +1 -1
  17. package/dist/acp/AcpProviderCatalogService.js +68 -2
  18. package/dist/acp/AcpProviderCatalogService.js.map +1 -1
  19. package/dist/acp/AcpRuntimeEventMapper.d.ts.map +1 -1
  20. package/dist/acp/AcpRuntimeEventMapper.js +9 -0
  21. package/dist/acp/AcpRuntimeEventMapper.js.map +1 -1
  22. package/dist/acp/AcpSessionService.d.ts +36 -0
  23. package/dist/acp/AcpSessionService.d.ts.map +1 -1
  24. package/dist/acp/AcpSessionService.js +57 -1
  25. package/dist/acp/AcpSessionService.js.map +1 -1
  26. package/dist/acp/DurableAcpPermissionPolicyStore.d.ts.map +1 -1
  27. package/dist/acp/DurableAcpPermissionPolicyStore.js +5 -3
  28. package/dist/acp/DurableAcpPermissionPolicyStore.js.map +1 -1
  29. package/dist/acp/acpRedaction.d.ts.map +1 -1
  30. package/dist/acp/acpRedaction.js +2 -0
  31. package/dist/acp/acpRedaction.js.map +1 -1
  32. package/dist/acp/acpTypes.d.ts +111 -1
  33. package/dist/acp/acpTypes.d.ts.map +1 -1
  34. package/dist/acp/acpTypes.js +21 -1
  35. package/dist/acp/acpTypes.js.map +1 -1
  36. package/dist/index.d.ts +108 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +236 -11
  39. package/dist/index.js.map +1 -1
  40. package/dist/isolation/CommandExecution.d.ts +7 -12
  41. package/dist/isolation/CommandExecution.d.ts.map +1 -1
  42. package/dist/isolation/CommandExecution.js +7 -12
  43. package/dist/isolation/CommandExecution.js.map +1 -1
  44. package/dist/isolation/CommandStream.d.ts +100 -0
  45. package/dist/isolation/CommandStream.d.ts.map +1 -0
  46. package/dist/isolation/CommandStream.js +47 -0
  47. package/dist/isolation/CommandStream.js.map +1 -0
  48. package/dist/isolation/RunExecutionEnvironment.d.ts +15 -0
  49. package/dist/isolation/RunExecutionEnvironment.d.ts.map +1 -1
  50. package/dist/isolation/RunExecutionEnvironment.js +45 -9
  51. package/dist/isolation/RunExecutionEnvironment.js.map +1 -1
  52. package/dist/isolation/RunIsolationService.d.ts +10 -5
  53. package/dist/isolation/RunIsolationService.d.ts.map +1 -1
  54. package/dist/isolation/RunIsolationService.js +7 -3
  55. package/dist/isolation/RunIsolationService.js.map +1 -1
  56. package/dist/isolation/SandboxCapabilities.d.ts +28 -0
  57. package/dist/isolation/SandboxCapabilities.d.ts.map +1 -0
  58. package/dist/isolation/SandboxCapabilities.js +33 -0
  59. package/dist/isolation/SandboxCapabilities.js.map +1 -0
  60. package/dist/isolation/SandboxCommandExecutor.d.ts +4 -7
  61. package/dist/isolation/SandboxCommandExecutor.d.ts.map +1 -1
  62. package/dist/isolation/SandboxCommandExecutor.js +6 -7
  63. package/dist/isolation/SandboxCommandExecutor.js.map +1 -1
  64. package/dist/isolation/SandboxCommandStream.d.ts +38 -0
  65. package/dist/isolation/SandboxCommandStream.d.ts.map +1 -0
  66. package/dist/isolation/SandboxCommandStream.js +232 -0
  67. package/dist/isolation/SandboxCommandStream.js.map +1 -0
  68. package/dist/isolation/shellArgv.d.ts +15 -0
  69. package/dist/isolation/shellArgv.d.ts.map +1 -0
  70. package/dist/isolation/shellArgv.js +31 -0
  71. package/dist/isolation/shellArgv.js.map +1 -0
  72. package/dist/isolation/useIdempotency.d.ts +19 -0
  73. package/dist/isolation/useIdempotency.d.ts.map +1 -0
  74. package/dist/isolation/useIdempotency.js +57 -0
  75. package/dist/isolation/useIdempotency.js.map +1 -0
  76. package/dist/localRpcTransport.d.ts +18 -0
  77. package/dist/localRpcTransport.d.ts.map +1 -1
  78. package/dist/localRpcTransport.js +38 -4
  79. package/dist/localRpcTransport.js.map +1 -1
  80. package/dist/node.d.ts +19 -0
  81. package/dist/node.d.ts.map +1 -1
  82. package/dist/node.js +19 -0
  83. package/dist/node.js.map +1 -1
  84. package/dist/observation/XenoUseRunObserver.d.ts +20 -0
  85. package/dist/observation/XenoUseRunObserver.d.ts.map +1 -1
  86. package/dist/observation/XenoUseRunObserver.js +27 -13
  87. package/dist/observation/XenoUseRunObserver.js.map +1 -1
  88. package/dist/runtime/RuntimeTimelineProjection.d.ts +2 -2
  89. package/dist/runtime/RuntimeTimelineProjection.d.ts.map +1 -1
  90. package/dist/runtime/RuntimeTimelineProjection.js +196 -1
  91. package/dist/runtime/RuntimeTimelineProjection.js.map +1 -1
  92. package/dist/sdk/DurablePendingInjectionStore.d.ts +24 -0
  93. package/dist/sdk/DurablePendingInjectionStore.d.ts.map +1 -0
  94. package/dist/sdk/DurablePendingInjectionStore.js +103 -0
  95. package/dist/sdk/DurablePendingInjectionStore.js.map +1 -0
  96. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts +81 -1
  97. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts.map +1 -1
  98. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js +122 -3
  99. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js.map +1 -1
  100. package/dist/sdk/pendingInjections.d.ts +83 -0
  101. package/dist/sdk/pendingInjections.d.ts.map +1 -0
  102. package/dist/sdk/pendingInjections.js +79 -0
  103. package/dist/sdk/pendingInjections.js.map +1 -0
  104. package/dist/session/AgentSessionService.d.ts +34 -0
  105. package/dist/session/AgentSessionService.d.ts.map +1 -1
  106. package/dist/session/AgentSessionService.js +85 -1
  107. package/dist/session/AgentSessionService.js.map +1 -1
  108. package/dist/session/nativeTuiSessionPty.d.ts +40 -0
  109. package/dist/session/nativeTuiSessionPty.d.ts.map +1 -0
  110. package/dist/session/nativeTuiSessionPty.js +142 -0
  111. package/dist/session/nativeTuiSessionPty.js.map +1 -0
  112. package/dist/session/nodePtyLoader.d.ts +23 -0
  113. package/dist/session/nodePtyLoader.d.ts.map +1 -0
  114. package/dist/session/nodePtyLoader.js +41 -0
  115. package/dist/session/nodePtyLoader.js.map +1 -0
  116. package/dist/session/renderSupport.d.ts +34 -0
  117. package/dist/session/renderSupport.d.ts.map +1 -1
  118. package/dist/session/renderSupport.js +39 -1
  119. package/dist/session/renderSupport.js.map +1 -1
  120. package/dist/workspace/WorkspaceHostService.d.ts +27 -2
  121. package/dist/workspace/WorkspaceHostService.d.ts.map +1 -1
  122. package/dist/workspace/WorkspaceHostService.js +221 -25
  123. package/dist/workspace/WorkspaceHostService.js.map +1 -1
  124. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { AgentHostProtocolError, XENO_AGENT_HOST_MIN_PROTOCOL_VERSION, XENO_AGEN
8
8
  const MAX_FLEET_COLLISIONS = 500;
9
9
  const MAX_QUEUED_INSTRUCTIONS = 50;
10
10
  const MAX_QUEUED_INSTRUCTION_LENGTH = 20_000;
11
- import { cloneProvider, isDevelopmentProviderReady, missingDevelopmentCapabilities, resolveAgentRuntimeSelection, renderWorkspaceKnowledge, selectAvailableProviders, withHostBackedCapabilities, planFanOut, routeByCapability, } from '@xenosystem/agent-interface-core';
11
+ import { cloneProvider, isDevelopmentProviderReady, missingDevelopmentCapabilities, resolveAgentRuntimeSelection, renderWorkspaceKnowledge, selectAvailableProviders, selectSessionRenderMode, withHostBackedCapabilities, planFanOut, routeByCapability, } from '@xenosystem/agent-interface-core';
12
12
  import { InMemoryInterruptedTurnStore, } from './turns/interruptedTurns.js';
13
13
  import { evaluateTurnAdmission, resolveTurnAdmissionPolicy, } from './turns/turnAdmission.js';
14
14
  import { boundSubagentWorkspace } from './subagents/subagentWorkspaceAuthority.js';
@@ -23,6 +23,7 @@ import { IntakeService } from './intake/IntakeService.js';
23
23
  import { WorkspaceKnowledgeService } from './knowledge/WorkspaceKnowledgeService.js';
24
24
  import { AgentSessionService } from './session/AgentSessionService.js';
25
25
  import { createBufferedSessionPty } from './session/bufferedSessionPty.js';
26
+ import { createNativeTuiSessionPty } from './session/nativeTuiSessionPty.js';
26
27
  import { renderSupportFor } from './session/renderSupport.js';
27
28
  import { createRuntimeEventSessionChannel } from './session/runtimeEventChannel.js';
28
29
  import { answerChooseDirectoryRequest } from './agent/chooseDirectoryBridge.js';
@@ -74,6 +75,8 @@ export class AgentHostCoordinator {
74
75
  runIsolationService;
75
76
  runObservationBinder;
76
77
  sessionService;
78
+ nativeTuiLane;
79
+ acpAgentRegistry;
77
80
  intakeService;
78
81
  knowledgeService;
79
82
  acpApprovalService;
@@ -95,6 +98,18 @@ export class AgentHostCoordinator {
95
98
  */
96
99
  turnTimers = new Map();
97
100
  activeTurns = new Map();
101
+ /**
102
+ * The lane a conversation last ran on, kept AFTER the turn ends.
103
+ *
104
+ * 🔴 `activeTurns` cannot answer this and that is not an oversight — it is keyed by
105
+ * `(conversationId, requestId)` because everything it serves is turn-scoped. Pin-forward
106
+ * (§2.2) is normally a BETWEEN-turns act, so resolving its target from `activeTurns` would make
107
+ * the feature work only during the one window where the user is least likely to want it.
108
+ *
109
+ * ⚠️ Deliberately never cleared on turn end. "Which agent is this conversation running on" stays
110
+ * true between turns; clearing it would reintroduce the turn scope this map exists to escape.
111
+ */
112
+ conversationAdapters = new Map();
98
113
  commandResults = new Map();
99
114
  /**
100
115
  * Instructions stacked while a turn is running, per conversation.
@@ -115,7 +130,7 @@ export class AgentHostCoordinator {
115
130
  providerCatalogFingerprint = '';
116
131
  providerCatalogRefresh = null;
117
132
  constructor(options) {
118
- const { providers, providerCatalogLoader, runtimeEvents, runtimeEventRepository, stateRepository, workspaceService, interactionService, permissionPolicyService, interruptedTurnStore, turnAdmissionPolicy, turnAbandonGraceMs, sdkControlService, engineeringControlService, turnExecutionAdapters, coordinationService, runObservationService, runIsolationService, runObservationBinder, sessionService, intakeService, knowledgeService, acpApprovalService, subagentRuns, resolveRunId, ...hostOptions } = options;
133
+ const { providers, providerCatalogLoader, runtimeEvents, runtimeEventRepository, stateRepository, workspaceService, interactionService, permissionPolicyService, interruptedTurnStore, turnAdmissionPolicy, turnAbandonGraceMs, sdkControlService, engineeringControlService, turnExecutionAdapters, coordinationService, runObservationService, runIsolationService, runObservationBinder, sessionService, nativeTuiLane, acpAgentRegistry, intakeService, knowledgeService, acpApprovalService, subagentRuns, resolveRunId, ...hostOptions } = options;
119
134
  if (runtimeEventRepository && runtimeEvents && runtimeEvents.length > 0) {
120
135
  throw new Error('Seeded runtimeEvents cannot be combined with a durable runtimeEventRepository.');
121
136
  }
@@ -187,6 +202,8 @@ export class AgentHostCoordinator {
187
202
  // Counts and completeness only — see the contract's `run.observation`.
188
203
  this.runObservationService?.setEventSink((summary) => this.publish('run.observation', summary));
189
204
  this.sessionService = sessionService;
205
+ this.nativeTuiLane = nativeTuiLane;
206
+ this.acpAgentRegistry = acpAgentRegistry;
190
207
  this.intakeService = intakeService;
191
208
  this.knowledgeService = knowledgeService;
192
209
  // Session lifecycle on the feed: ids, modes and viewer counts — never PTY
@@ -651,8 +668,38 @@ export class AgentHostCoordinator {
651
668
  if (!service)
652
669
  return;
653
670
  try {
654
- const support = renderSupportFor(provider, { adapters: [...this.turnExecutionAdapters.values()] });
655
- const protocol = createRuntimeEventSessionChannel({
671
+ const support = renderSupportFor(provider, {
672
+ adapters: [...this.turnExecutionAdapters.values()],
673
+ // The seam `renderSupport.ts` declares. Undefined falls back to
674
+ // `noNativeTuiLane`, so the default is unchanged.
675
+ // The predicate is DERIVED from the command, so support cannot be
676
+ // claimed for a provider we could not actually launch.
677
+ ...(this.nativeTuiLane
678
+ ? { nativeTuiLane: (candidate) => Boolean(this.nativeTuiLane?.(candidate)) }
679
+ : {}),
680
+ });
681
+ /**
682
+ * 🔴 Ask which mode before building the channel, because the two modes
683
+ * are EXCLUSIVE and this call site used to build both.
684
+ *
685
+ * `open()` refuses `native-tui` when a protocol channel is present — a
686
+ * CLI in interactive TUI mode is not also serving ACP. Passing `protocol`
687
+ * unconditionally was harmless only while `nativeTuiLane` was hardcoded
688
+ * false: the moment a lane said true, `selectSessionRenderMode` would
689
+ * pick `native-tui` and this site would hand it the one thing that makes
690
+ * it illegal, so EVERY session on that provider would be refused with
691
+ * `protocol_channel_forbidden`.
692
+ *
693
+ * That is the exact failure `renderSupport.ts` promised could not happen
694
+ * — "the mode becomes available by construction". Construction reached as
695
+ * far as `renderSupportFor` and stopped one caller short.
696
+ *
697
+ * ⚠️ This calls the SAME exported selector `open()` calls, rather than
698
+ * restating its rule. Two callers of one function cannot drift; two
699
+ * copies of a rule can.
700
+ */
701
+ const selection = selectSessionRenderMode(support);
702
+ const createProtocol = () => createRuntimeEventSessionChannel({
656
703
  runId,
657
704
  subscribe: (listener) => this.subscribe((event) => {
658
705
  if (event.type !== 'runtime.event.appended')
@@ -662,23 +709,94 @@ export class AgentHostCoordinator {
662
709
  listener(payload.event);
663
710
  }),
664
711
  });
712
+ const protocol = selection.ok && selection.mode === 'acp' ? createProtocol() : undefined;
713
+ /**
714
+ * 🔴 The PTY is chosen by MODE, and the two are not interchangeable.
715
+ *
716
+ * `acp` gets a render target — our drawing of the structured stream, with
717
+ * a `write` that THROWS because a keystroke reaching it means a mode rule
718
+ * broke. `native-tui` gets a real terminal with the vendor's own binary
719
+ * inside it, where a write is a keystroke and must land.
720
+ *
721
+ * Handing the wrong one over is the single most likely way this mode goes
722
+ * wrong, which is why the buffered pty was built to fail loudly rather
723
+ * than no-op.
724
+ */
725
+ const spawnRequest = selection.ok && selection.mode === 'native-tui'
726
+ ? this.nativeTuiLane?.(provider)
727
+ : undefined;
665
728
  const opened = service.open({
666
729
  sessionId: runId,
667
730
  runId,
668
731
  support,
669
- pty: createBufferedSessionPty(),
670
- protocol,
732
+ pty: spawnRequest ? createNativeTuiSessionPty(spawnRequest) : createBufferedSessionPty(),
733
+ ...(protocol ? { protocol } : {}),
734
+ /**
735
+ * Passed even when a channel was built above, because `session.setMode`
736
+ * CLOSES the channel on the way to `native-tui` and a closed one cannot
737
+ * be revived. Without the factory the switch would be one-way, and a
738
+ * user who tried the agent's own TUI could never get the structured
739
+ * view back.
740
+ */
741
+ createProtocol,
671
742
  });
672
743
  // A refusal is the substrate doing its job — an unexecutable provider has
673
744
  // nothing to render. Close the channel we built for it rather than leaving
674
745
  // a subscription attached to a session that never existed.
675
746
  if (!opened.ok)
676
- protocol.close();
747
+ protocol?.close();
677
748
  }
678
749
  catch {
679
750
  // Contained: see above. The run proceeds without a session.
680
751
  }
681
752
  }
753
+ /**
754
+ * ADE §2.8's switch. The substrate has always accepted a preference; until
755
+ * `session.setMode` existed, no client could express one.
756
+ */
757
+ /**
758
+ * The slice of the ACP registry this host needs — a PORT, not the service.
759
+ *
760
+ * Declared structurally so `packages/host`'s public surface does not drag the
761
+ * registry's own types across the protocol boundary, and so a composition can
762
+ * supply something else entirely.
763
+ */
764
+ listAcpAgents() {
765
+ const registry = this.acpAgentRegistry;
766
+ // "No agents" and "this host cannot manage agents" are different facts, and
767
+ // a client that conflates them shows an empty picker with no way forward.
768
+ if (!registry)
769
+ return { ok: false, agents: [], reason: 'This host has no ACP agent registry mounted.' };
770
+ return { ok: true, agents: registry.listSummaries() };
771
+ }
772
+ addAcpPreset(request) {
773
+ const registry = this.acpAgentRegistry;
774
+ if (!registry)
775
+ return { ok: false, reason: 'This host has no ACP agent registry mounted.' };
776
+ // 🔴 Validated against the registry's OWN list, not a copy of it here. A
777
+ // second list of preset ids would be free to drift from the one that
778
+ // decides — which is exactly how the previous guard came to name three
779
+ // presets while six existed.
780
+ if (!registry.isPresetId(request.presetId)) {
781
+ return {
782
+ ok: false,
783
+ reason: `Unknown agent preset '${request.presetId}'. Available: ${registry.presetIds().join(', ')}.`,
784
+ };
785
+ }
786
+ return registry.addPreset(request.presetId);
787
+ }
788
+ removeAcpAgent(request) {
789
+ const registry = this.acpAgentRegistry;
790
+ if (!registry)
791
+ return { ok: false, removed: false, reason: 'This host has no ACP agent registry mounted.' };
792
+ return registry.remove(request.agentId);
793
+ }
794
+ setSessionMode(request) {
795
+ const service = this.sessionService;
796
+ if (!service)
797
+ return { ok: false, reason: 'This host has no session substrate mounted.' };
798
+ return service.setMode(request.sessionId, request.mode);
799
+ }
682
800
  async routeSessionInput(input) {
683
801
  const service = this.sessionService;
684
802
  if (!service)
@@ -1072,7 +1190,7 @@ export class AgentHostCoordinator {
1072
1190
  // 🔴 ALWAYS present, unlike every other optional field on this row.
1073
1191
  // §5.3 requires the user to always know whether the thing running is
1074
1192
  // contained, and "always" is the word that forbids omitting it when the
1075
- // answer is unflattering. `none` IS the answer today.
1193
+ // answer is unflattering. `none` is a real answer, not a missing field.
1076
1194
  isolation: isolation.isolation,
1077
1195
  ...(isolation.mechanism === undefined ? {} : { isolationMechanism: isolation.mechanism }),
1078
1196
  ...(isolation.reason === undefined ? {} : { isolationReason: isolation.reason }),
@@ -1413,6 +1531,9 @@ export class AgentHostCoordinator {
1413
1531
  if (method === 'turn.cancel') {
1414
1532
  return await this.cancelTurn(input);
1415
1533
  }
1534
+ if (method === 'context.inject') {
1535
+ return await this.injectContext(input);
1536
+ }
1416
1537
  if (method === 'turn.steer') {
1417
1538
  return await this.steerTurn(input);
1418
1539
  }
@@ -1470,6 +1591,18 @@ export class AgentHostCoordinator {
1470
1591
  if (method === 'session.release') {
1471
1592
  return this.releaseSession(input);
1472
1593
  }
1594
+ if (method === 'acp.agents.list') {
1595
+ return this.listAcpAgents();
1596
+ }
1597
+ if (method === 'acp.agents.addPreset') {
1598
+ return this.addAcpPreset(input);
1599
+ }
1600
+ if (method === 'acp.agents.remove') {
1601
+ return this.removeAcpAgent(input);
1602
+ }
1603
+ if (method === 'session.setMode') {
1604
+ return this.setSessionMode(input);
1605
+ }
1473
1606
  if (method === 'intake.sources.list') {
1474
1607
  return this.listIntakeSources();
1475
1608
  }
@@ -2025,6 +2158,7 @@ export class AgentHostCoordinator {
2025
2158
  }
2026
2159
  const controller = new AbortController();
2027
2160
  this.activeTurns.set(activeKey, { adapter, controller, request });
2161
+ this.conversationAdapters.set(request.conversationId, adapter);
2028
2162
  // 🔴 The host ENFORCES the deadline it already validates. `timeoutMs` was checked on the way
2029
2163
  // in (1..3_600_000) and forwarded to the adapter, but nothing here ever acted on it: the
2030
2164
  // controller was aborted only for shutdown or an explicit cancel. A turn whose adapter
@@ -2342,9 +2476,21 @@ export class AgentHostCoordinator {
2342
2476
  const failure = {
2343
2477
  success: false,
2344
2478
  code: 'steer_unsupported',
2479
+ /**
2480
+ * 🔴 NAME THE LANE, exactly as the no-steer branch above does (§7.1 capability
2481
+ * honesty). This said “The provider rejected the correction”, which names nothing an
2482
+ * operator can act on — with several lanes on a host, “the provider” is the one fact
2483
+ * they need and the one this withheld.
2484
+ *
2485
+ * ⚠️ IT ONLY BECAME REACHABLE WHEN THE ACP LANE LEARNED TO STEER. Until then no ACP
2486
+ * adapter implemented `steer`, so every ACP refusal took the branch above, which does
2487
+ * name itself. Implementing the capability moved the refusal onto a path whose message
2488
+ * was weaker — an improvement that quietly regressed the diagnostics beside it.
2489
+ * `scripts/session-attach-probe.mjs` caught it; nothing in this repo did.
2490
+ */
2345
2491
  safeError: error instanceof Error && error.message
2346
- ? `The provider rejected the correction: ${error.message.slice(0, 300)}`
2347
- : 'The provider rejected the correction.',
2492
+ ? `The '${active.adapter.id}' lane rejected the correction: ${error.message.slice(0, 300)}`
2493
+ : `The '${active.adapter.id}' lane rejected the correction.`,
2348
2494
  conversationId: input.conversationId,
2349
2495
  requestId: input.requestId,
2350
2496
  };
@@ -2362,6 +2508,86 @@ export class AgentHostCoordinator {
2362
2508
  this.commandResults.set(commandKey, { method: 'turn.steer', fingerprint, result: structuredClone(result) });
2363
2509
  return result;
2364
2510
  }
2511
+ /**
2512
+ * `context.inject` — push a pinned canvas node back into an agent's context (XENO SPAWN §2.2).
2513
+ *
2514
+ * 🔴 THE COMMAND THAT DID NOT EXIST. Both lanes could already inject — `AgentLoop.injectContext()`
2515
+ * on the SDK side, `AgentProcess.injectContext()` on the ACP side — the `context.injected` event
2516
+ * was defined, forwarded and PROJECTED, and nothing in this host could trigger any of it. Every
2517
+ * layer was built and tested; the one that lets a surface act was missing, which is the shape
2518
+ * this ecosystem keeps re-finding.
2519
+ *
2520
+ * ⚠️ NOT gated on an active turn, unlike `steerTurn`. Pin-forward is normally a between-turns
2521
+ * act, so the lane is resolved from `conversationAdapters` rather than `activeTurns`.
2522
+ */
2523
+ async injectContext(input) {
2524
+ if (!this.isConnectedClient(input.command.client)) {
2525
+ return {
2526
+ success: false,
2527
+ code: 'client_not_connected',
2528
+ safeError: 'Complete the handshake before injecting context.',
2529
+ conversationId: input.conversationId,
2530
+ };
2531
+ }
2532
+ if (typeof input.content !== 'string' || !input.content.trim()) {
2533
+ return {
2534
+ success: false,
2535
+ code: 'invalid_request',
2536
+ safeError: 'Injected content must be a non-empty string.',
2537
+ conversationId: input.conversationId,
2538
+ };
2539
+ }
2540
+ const adapter = this.conversationAdapters.get(input.conversationId);
2541
+ if (!adapter) {
2542
+ // Nothing has run on this conversation yet, so there is no agent to hold the node. Said
2543
+ // plainly rather than as a generic failure: the remedy is to start a turn first.
2544
+ return {
2545
+ success: false,
2546
+ code: 'no_active_session',
2547
+ safeError: 'This conversation has no agent session yet. Start a turn before pinning a node into it.',
2548
+ conversationId: input.conversationId,
2549
+ };
2550
+ }
2551
+ if (!adapter.injectContext) {
2552
+ /**
2553
+ * 🔴 The remedy is DERIVED from what the other lanes can do, not written by hand — the same
2554
+ * rule `steerTurn` follows, so a lane gaining the capability changes this message for free.
2555
+ */
2556
+ const capable = [...this.turnExecutionAdapters.values()]
2557
+ .filter((entry) => entry.injectContext)
2558
+ .map((entry) => entry.id);
2559
+ return {
2560
+ success: false,
2561
+ code: 'inject_unsupported',
2562
+ safeError: capable.length > 0
2563
+ ? `The '${adapter.id}' lane cannot take injected context. Lanes that can: ${capable.join(', ')}.`
2564
+ : `The '${adapter.id}' lane cannot take injected context, and no lane on this host can.`,
2565
+ conversationId: input.conversationId,
2566
+ };
2567
+ }
2568
+ try {
2569
+ const outcome = await adapter.injectContext(structuredClone(input));
2570
+ return {
2571
+ success: true,
2572
+ ...(outcome?.messageId ? { messageId: outcome.messageId } : {}),
2573
+ ...(outcome?.status ? { status: outcome.status } : {}),
2574
+ deliveredVia: 'adapter',
2575
+ conversationId: input.conversationId,
2576
+ };
2577
+ }
2578
+ catch (error) {
2579
+ // Reported, never swallowed. A caller that believes a node was pinned when it was not will
2580
+ // reason about what the agent knows using an edge that does not exist.
2581
+ return {
2582
+ success: false,
2583
+ code: 'inject_unsupported',
2584
+ safeError: error instanceof Error && error.message
2585
+ ? `The provider rejected the injection: ${error.message.slice(0, 300)}`
2586
+ : 'The provider rejected the injection.',
2587
+ conversationId: input.conversationId,
2588
+ };
2589
+ }
2590
+ }
2365
2591
  /** True while any turn is running for this conversation. */
2366
2592
  hasActiveTurn(conversationId) {
2367
2593
  for (const active of this.activeTurns.values()) {
@@ -3156,7 +3382,6 @@ export * from './isolation/RunIsolationService.js';
3156
3382
  */
3157
3383
  export * from './isolation/SandboxCommandExecutor.js';
3158
3384
  export * from './observation/XenoUseObservationSource.js';
3159
- export * from './observation/XenoUseRunObserver.js';
3160
3385
  export * from './session/AgentSessionService.js';
3161
3386
  export * from './session/runtimeEventChannel.js';
3162
3387
  export * from './session/renderSupport.js';