@borgee/agents-host 0.2.97 → 0.2.110

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 (104) hide show
  1. package/README.md +77 -5
  2. package/dist/agents-host.d.ts +16 -0
  3. package/dist/agents-host.js +608 -82
  4. package/dist/catalog-worker.js +21 -0
  5. package/dist/catalog-worker.js.map +7 -0
  6. package/dist/chat/chat-control-plane.d.ts +19 -4
  7. package/dist/chat/normalized-input.d.ts +9 -0
  8. package/dist/chat/normalized-input.js +24 -0
  9. package/dist/chat/sdk-chat-control-plane.d.ts +46 -6
  10. package/dist/chat/sdk-chat-control-plane.js +169 -6
  11. package/dist/cli-args.d.ts +1 -1
  12. package/dist/cli-args.js +13 -4
  13. package/dist/config.d.ts +3 -1
  14. package/dist/config.js +25 -0
  15. package/dist/context/injection.d.ts +16 -0
  16. package/dist/context/injection.js +37 -21
  17. package/dist/context/prompt.d.ts +12 -0
  18. package/dist/context/prompt.js +25 -0
  19. package/dist/context/skill-manual.js +1 -1
  20. package/dist/context/turn-preparation.d.ts +2 -1
  21. package/dist/context/turn-preparation.js +15 -0
  22. package/dist/execution-telemetry.d.ts +96 -0
  23. package/dist/execution-telemetry.js +583 -0
  24. package/dist/gateway/channel-file-workspace.d.ts +15 -0
  25. package/dist/gateway/channel-file-workspace.js +130 -0
  26. package/dist/gateway/localhost-gateway.js +167 -2
  27. package/dist/hosted-turn-content.js +3 -2
  28. package/dist/local-config.js +34 -1
  29. package/dist/managed-daemon.js +15 -0
  30. package/dist/native-environment.d.ts +2 -0
  31. package/dist/native-environment.js +9 -0
  32. package/dist/native-protocol-types/codec.d.ts +49 -0
  33. package/dist/native-protocol-types/commands.d.ts +71 -0
  34. package/dist/native-protocol-types/compatibility.d.ts +5 -0
  35. package/dist/native-protocol-types/cursor.d.ts +7 -0
  36. package/dist/native-protocol-types/envelope.d.ts +1105 -0
  37. package/dist/native-protocol-types/file-changes.d.ts +20 -0
  38. package/dist/native-protocol-types/history.d.ts +487 -0
  39. package/dist/native-protocol-types/index.d.ts +18 -0
  40. package/dist/native-protocol-types/interactions.d.ts +619 -0
  41. package/dist/native-protocol-types/messages.d.ts +2482 -0
  42. package/dist/native-protocol-types/public-validation.d.ts +10 -0
  43. package/dist/native-protocol-types/remote-host-uplink.d.ts +67 -0
  44. package/dist/native-protocol-types/resources.d.ts +126 -0
  45. package/dist/native-protocol-types/session-settings.d.ts +22 -0
  46. package/dist/native-protocol-types/snapshot.d.ts +575 -0
  47. package/dist/native-protocol-types/timeline.d.ts +1503 -0
  48. package/dist/native-protocol-types/tool-result.d.ts +26 -0
  49. package/dist/native-protocol-types/uplink.d.ts +56 -0
  50. package/dist/native-protocol-types/version.d.ts +5 -0
  51. package/dist/native-provider-types/commands.d.ts +51 -0
  52. package/dist/native-provider-types/control.d.ts +192 -0
  53. package/dist/native-provider-types/file-changes.d.ts +9 -0
  54. package/dist/native-provider-types/index.d.ts +8 -0
  55. package/dist/native-provider-types/interactions.d.ts +7 -0
  56. package/dist/native-provider-types/observation.d.ts +209 -0
  57. package/dist/native-provider-types/provider.d.ts +124 -0
  58. package/dist/native-provider-types/session-settings.d.ts +17 -0
  59. package/dist/native-provider-types/testing.d.ts +27 -0
  60. package/dist/native-provider-types/tool-result.d.ts +21 -0
  61. package/dist/native-providers.d.ts +21 -0
  62. package/dist/native-providers.js +18402 -0
  63. package/dist/native-providers.js.map +7 -0
  64. package/dist/plugin-sdk.js +10713 -229
  65. package/dist/plugin-sdk.js.map +4 -4
  66. package/dist/policy/copilot-permission.js +1 -8
  67. package/dist/policy/gateway-authorization.d.ts +1 -1
  68. package/dist/policy/gateway-authorization.js +22 -2
  69. package/dist/providers/claude/adapter.js +1 -0
  70. package/dist/providers/claude/cli-client.js +4 -0
  71. package/dist/providers/codex/adapter.js +1 -0
  72. package/dist/providers/codex/cli-client.js +5 -0
  73. package/dist/providers/copilot/adapter.js +4 -0
  74. package/dist/providers/copilot/cli-client.js +11 -1
  75. package/dist/providers/copilot/sdk-session.d.ts +12 -3
  76. package/dist/providers/copilot/sdk-session.js +230 -6
  77. package/dist/providers/create-provider.js +24 -0
  78. package/dist/providers/normalized/adapter.d.ts +107 -0
  79. package/dist/providers/normalized/adapter.js +1650 -0
  80. package/dist/providers/normalized/control-policy.d.ts +5 -0
  81. package/dist/providers/normalized/control-policy.js +12 -0
  82. package/dist/providers/normalized/interactions.d.ts +3 -0
  83. package/dist/providers/normalized/interactions.js +39 -0
  84. package/dist/providers/normalized/legacy-input-ownership.d.ts +34 -0
  85. package/dist/providers/normalized/legacy-input-ownership.js +123 -0
  86. package/dist/providers/normalized/session-management.d.ts +67 -0
  87. package/dist/providers/normalized/session-management.js +334 -0
  88. package/dist/providers/normalized/session-ownership.d.ts +3 -0
  89. package/dist/providers/normalized/session-ownership.js +90 -0
  90. package/dist/providers/prompt-usage.d.ts +8 -0
  91. package/dist/providers/prompt-usage.js +52 -0
  92. package/dist/providers/provider-adapter.d.ts +43 -1
  93. package/dist/providers/provider-adapter.js +8 -0
  94. package/dist/state-paths.d.ts +2 -0
  95. package/dist/state-paths.js +6 -0
  96. package/dist/types.d.ts +66 -0
  97. package/dist/typing-lease.d.ts +14 -0
  98. package/dist/typing-lease.js +31 -0
  99. package/dist/vendor/agent-provider-codex/LICENSE +211 -0
  100. package/dist/vendor/agent-provider-codex/NOTICE +13 -0
  101. package/package.json +22 -13
  102. package/skills/borgee-agent/SKILL.md +12 -4
  103. package/skills/borgee-agent/scripts/borgee-agent.mjs +110 -0
  104. package/skills/borgee-agent/scripts/borgee-agent.py +116 -0
@@ -1,3 +1,4 @@
1
+ import { readNormalizedInputText, readDurableInputText } from './chat/normalized-input.js';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { chmod, link, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
3
4
  import { dirname, join, resolve } from 'node:path';
@@ -5,6 +6,10 @@ import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
5
6
  import { HostLogger, summarizeError } from './debug.js';
6
7
  import { toReportedActivity } from './progress-to-activity.js';
7
8
  import { BackgroundRunTracker } from './background-runs.js';
9
+ import { ExecutionDeliveryJournal, ExecutionTelemetryReporter, } from './execution-telemetry.js';
10
+ import { startTypingLease } from './typing-lease.js';
11
+ import { recordProtocolTrace, NormalizedActivityPublisher, NormalizedExecutionTelemetry, sessionControlResponse, parseSessionControlRequest } from './plugin-sdk.js';
12
+ import { respondToHostedInteraction } from './providers/normalized/interactions.js';
8
13
  import { ATTENTION_FOLLOW_SEMANTICS_COMPATIBILITY_GATE, COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE, COLLABORATION_OUTCOME_MODEL_COMPATIBILITY_GATE, COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE, CONTEXT_INJECTION_COMPATIBILITY_GATE, LOCALHOST_GATEWAY_COMPATIBILITY_GATE, POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE, resolveInternalPolicyMode, resolveInternalCompatibilityGates, SKILL_RUNTIME_COMPATIBILITY_GATE, TASK_THREAD_COLLABORATION_CONTRACT_COMPATIBILITY_GATE, TOKEN_BINDING_COMPATIBILITY_GATE, } from './compatibility-gates.js';
9
14
  import { buildNormalizedAttentionSnapshot, parsePersistedAttentionSnapshot, } from './context/attention.js';
10
15
  import { mergeCompactionSnapshot, parsePersistedCompactionSnapshot, } from './context/compaction.js';
@@ -13,8 +18,8 @@ import { createLocalhostGatewayController, LOCALHOST_GATEWAY_COLLABORATION_TARGE
13
18
  import { extractTaskIdFromTaskAssignmentContent, resolveTaskAssignmentStatePath, } from './context/injection.js';
14
19
  import { AuthorizationAuditSink, } from './policy/authorization-audit.js';
15
20
  import { createProvider } from './providers/create-provider.js';
16
- import { ProviderTurnCancelledError } from './providers/provider-adapter.js';
17
- import { resolveAttentionStatePath, resolveCompactionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
21
+ import { ProviderInputRejectedError, ProviderTurnCancelledError } from './providers/provider-adapter.js';
22
+ import { resolveAttentionStatePath, resolveCompactionStatePath, resolveExecutionDeliveryJournalPath, resolveExecutionTelemetryOutboxPath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
18
23
  import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
19
24
  import { buildHostedIncomingContentText, buildHostedTurnContentParts, extractHostedMessageAttachments, normalizeHostedMessageAttachment, } from './hosted-turn-content.js';
20
25
  import { waitForTaskForThread } from './task-thread-resolution.js';
@@ -110,6 +115,24 @@ class DefaultHostRuntime {
110
115
  deps.borgee ??
111
116
  new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
112
117
  pluginId: `agents-host:${config.agent.provider}`,
118
+ executionTelemetryOutboxPath: resolveExecutionTelemetryOutboxPath(config.stateRootDir),
119
+ onExecutionTelemetryError: (error) => {
120
+ deps.logger.error('execution telemetry transport failed', {
121
+ error: summarizeError(error),
122
+ });
123
+ },
124
+ providerOutputMode: () => this.provider.outputMode ?? 'legacy',
125
+ connectionLiveness: () => config.agent.provider === 'codex' && this.provider.outputMode === 'normalized'
126
+ ? { probeIntervalMs: 30_000, pongTimeoutMs: 10_000, connectTimeoutMs: 30_000 }
127
+ : undefined,
128
+ normalizedActivitySync: () => this.provider.outputMode === 'normalized' && !!this.provider.syncNormalizedActivity,
129
+ sessionControl: () => this.provider.outputMode === 'normalized' && !!this.provider.controlSession,
130
+ sessionBindings: () => this.provider.outputMode === 'normalized' && !!this.provider.setSessionPublication,
131
+ sessionRelations: () => this.provider.outputMode === 'normalized' && !!this.provider.describeSession,
132
+ sessionManagement: () => this.provider.outputMode === 'normalized' && this.provider.supportsSessionManagement === true,
133
+ sessionDirectory: () => this.provider.outputMode === 'normalized' && !!this.provider.listSessions,
134
+ sessionObservation: () => this.provider.outputMode === 'normalized' && !!this.provider.observeSession,
135
+ sessionInputOperations: () => this.provider.outputMode === 'normalized' ? this.provider.sessionInputOperations ?? [] : [],
113
136
  });
114
137
  this.gateway = createLocalhostGatewayController({
115
138
  gateEnabled: localhostGatewayGateEnabled,
@@ -133,6 +156,12 @@ class DefaultHostRuntime {
133
156
  stateRootDir: config.stateRootDir,
134
157
  agentApiKey: config.agent.agentApiKey,
135
158
  resolveStableAgentId: deps.resolveStableAgentId,
159
+ providerOutputMode: config.providerOutputMode,
160
+ nativeProviderCommand: config.nativeProviderCommand,
161
+ sessionWorkspaceRoot: config.sessionWorkspaceRoot,
162
+ codexConnectionMode: config.codexConnectionMode,
163
+ codexSharedSocketPath: config.codexSharedSocketPath,
164
+ codexTrustShared: config.codexTrustShared,
136
165
  claudeCommand: config.claudeCommand,
137
166
  claudeArgs: config.claudeArgs,
138
167
  codexCommand: config.codexCommand,
@@ -150,16 +179,19 @@ class DefaultHostRuntime {
150
179
  });
151
180
  this.ensureStateRoot = deps.ensureStateRoot ?? ensurePrivateStateRoot;
152
181
  }
153
- async start(onMessage, onStopTurn, onStopBackgroundRun, onActivityTransportReady) {
182
+ async start(onMessage, onStopTurn, onStopBackgroundRun, onActivityTransportReady, onSessionControl) {
154
183
  await this.ensureStateRoot(this.config.stateRootDir);
155
184
  await this.gateway.start();
156
185
  try {
186
+ if (this.provider.outputMode === 'normalized')
187
+ this.controlPlane.onServerRequest?.((action, params, context) => action === 'session_control' && onSessionControl && context ? onSessionControl(parseSessionControlRequest(params), context) : respondToHostedInteraction(this.provider, action, params, context));
157
188
  this.controlPlane.onStopTurn?.(onStopTurn);
158
189
  this.controlPlane.onStopBackgroundRun?.(onStopBackgroundRun);
159
190
  this.controlPlane.onActivityTransportReady?.(onActivityTransportReady);
160
191
  await this.controlPlane.connect(onMessage);
161
192
  }
162
193
  catch (error) {
194
+ this.controlPlane.onServerRequest?.(undefined);
163
195
  this.controlPlane.onStopTurn?.(undefined);
164
196
  this.controlPlane.onStopBackgroundRun?.(undefined);
165
197
  this.controlPlane.onActivityTransportReady?.(undefined);
@@ -170,6 +202,7 @@ class DefaultHostRuntime {
170
202
  async stop() {
171
203
  let thrown;
172
204
  try {
205
+ this.controlPlane.onServerRequest?.(undefined);
173
206
  this.controlPlane.onStopTurn?.(undefined);
174
207
  this.controlPlane.onStopBackgroundRun?.(undefined);
175
208
  this.controlPlane.onActivityTransportReady?.(undefined);
@@ -286,11 +319,14 @@ const MAX_AUTONOMOUS_REPLY_DEDUPLICATION_KEYS = 256;
286
319
  */
287
320
  export class AgentsHost {
288
321
  config;
322
+ executionTelemetry;
323
+ normalizedStateOwners = new Map();
289
324
  runtime;
290
325
  provider;
291
326
  borgee;
292
327
  backgroundRuns;
293
328
  logger;
329
+ executionDeliveryJournal;
294
330
  contextInjectionEnabled;
295
331
  collaborationEnabled;
296
332
  collaborationOutcomeModelEnabled;
@@ -298,6 +334,8 @@ export class AgentsHost {
298
334
  taskThreadCollaborationContractEnabled;
299
335
  collaborationCapabilitiesDiagnosticsEnabled;
300
336
  activeTurns = new Set();
337
+ executionTelemetryByExecutionId = new Map();
338
+ executionDispatches = new Map();
301
339
  awaitingUserByChannel = new Map();
302
340
  collaborationOutcomeByChannel = new Map();
303
341
  /**
@@ -328,6 +366,9 @@ export class AgentsHost {
328
366
  constructor(config, deps) {
329
367
  this.config = config;
330
368
  this.logger = new HostLogger(deps?.runtimeOptions);
369
+ this.executionDeliveryJournal =
370
+ deps?.executionDeliveryJournal ??
371
+ new ExecutionDeliveryJournal(resolveExecutionDeliveryJournalPath(config.stateRootDir));
331
372
  const compatibilityGates = resolveInternalCompatibilityGates();
332
373
  const contextInjectionGateEnabled = compatibilityGates.has(CONTEXT_INJECTION_COMPATIBILITY_GATE);
333
374
  this.contextInjectionEnabled = contextInjectionGateEnabled;
@@ -392,7 +433,17 @@ export class AgentsHost {
392
433
  const bufferedMessages = [];
393
434
  let bufferingMessages = true;
394
435
  try {
436
+ await this.executionDeliveryJournal.load();
395
437
  this.provider.onAutonomousReply?.((reply) => this.dispatchAutonomousReply(reply));
438
+ this.provider.setSessionPublication?.({
439
+ supported: () => this.started && !this.controlPlaneClosed && this.borgee.sessionBindingsSupported === true,
440
+ publish: (channelId, activity) => {
441
+ if (!this.started || this.controlPlaneClosed || !this.borgee.sessionBindingsSupported || !this.borgee.reportActivity)
442
+ return false;
443
+ this.borgee.reportActivity({ channelId, activity });
444
+ return true;
445
+ },
446
+ });
396
447
  await this.runtime.start((message) => {
397
448
  if (!this.started) {
398
449
  return;
@@ -402,8 +453,24 @@ export class AgentsHost {
402
453
  return;
403
454
  }
404
455
  this.dispatchMessage(message);
405
- }, async (channelId) => this.stopTurn(channelId), async (channelId, runId) => this.backgroundRuns.stop(channelId, runId), () => this.backgroundRuns.replay());
456
+ }, async (channelId) => this.stopTurn(channelId), async (channelId, runId) => this.backgroundRuns.stop(channelId, runId), () => { this.provider.publishSessionBindings?.(); this.backgroundRuns.replay(); }, (request, context) => this.handleSessionControl(request, context));
406
457
  const agentId = await this.ensureSelfAgentId();
458
+ if (this.provider.outputMode === 'normalized' && this.borgee.reportExecutionTelemetryFrame && this.borgee.onExecutionTelemetryAck) {
459
+ try {
460
+ this.executionTelemetry = new NormalizedExecutionTelemetry({
461
+ outboxPath: join(this.config.stateRootDir, 'execution-telemetry.json'),
462
+ scope: { serverUrl: this.config.borgeeBaseUrl, agentId },
463
+ transport: {
464
+ report: frame => this.borgee.reportExecutionTelemetryFrame(frame),
465
+ onAck: handler => this.borgee.onExecutionTelemetryAck(handler),
466
+ onReady: handler => this.borgee.onExecutionTransportReady?.(handler) ?? (() => { }),
467
+ },
468
+ });
469
+ }
470
+ catch (error) {
471
+ this.logger.error('normalized accounting unavailable', { error: summarizeError(error) });
472
+ }
473
+ }
407
474
  bufferingMessages = false;
408
475
  while (bufferedMessages.length > 0) {
409
476
  const pendingMessages = bufferedMessages.splice(0, bufferedMessages.length);
@@ -463,6 +530,9 @@ export class AgentsHost {
463
530
  runtimeStopError = error;
464
531
  }
465
532
  await Promise.allSettled([...this.activeTurns]);
533
+ this.executionTelemetry?.dispose();
534
+ this.executionTelemetry = undefined;
535
+ this.normalizedStateOwners.clear();
466
536
  await Promise.allSettled(pendingCompactionSnapshotWrites);
467
537
  this.compactionSnapshotWriteQueueByChannel.clear();
468
538
  if (runtimeStopError) {
@@ -471,16 +541,165 @@ export class AgentsHost {
471
541
  }
472
542
  trackActiveTurn(task) {
473
543
  this.activeTurns.add(task);
474
- void task.finally(() => {
544
+ void task
545
+ .catch((error) => {
546
+ this.logger.error('dispatched turn failed', {
547
+ error: summarizeError(error),
548
+ });
549
+ })
550
+ .finally(() => {
475
551
  this.activeTurns.delete(task);
476
552
  });
477
553
  }
478
554
  dispatchMessage(message) {
479
- const task = this.collaborationEnabled
480
- ? this.handleCollaborationMessage(message)
481
- : this.enqueueChannelTurn(message.channel_id, () => this.handleLegacyMessage(message));
555
+ // Self echoes must not occupy the channel queue before explicit control admission.
556
+ if (this.selfAgentId && String(message.user_id ?? message.sender_id ?? '') === this.selfAgentId && message.message_type !== 'task_assignment')
557
+ return;
558
+ const executionId = message.execution_grant?.executionId;
559
+ const existing = executionId
560
+ ? this.executionDispatches.get(executionId)
561
+ : undefined;
562
+ if (existing) {
563
+ this.trackActiveTurn(existing);
564
+ return;
565
+ }
566
+ const run = () => message.sessionTarget
567
+ ? this.handleSelectedSessionMessage(message)
568
+ : this.collaborationEnabled
569
+ ? this.handleCollaborationMessage(message)
570
+ : this.enqueueChannelTurn(message.channel_id, () => this.handleLegacyMessage(message));
571
+ if (this.provider.outputMode === 'normalized') {
572
+ this.trackActiveTurn(run());
573
+ return;
574
+ }
575
+ const work = executionId
576
+ ? this.runClaimedExecution(message, run)
577
+ : this.runMessageWithExecutionTelemetry(message, run).then((result) => {
578
+ if (result.error)
579
+ throw result.error;
580
+ });
581
+ const task = executionId
582
+ ? work.finally(() => {
583
+ this.executionDispatches.delete(executionId);
584
+ })
585
+ : work;
586
+ if (executionId)
587
+ this.executionDispatches.set(executionId, task);
482
588
  this.trackActiveTurn(task);
483
589
  }
590
+ async runClaimedExecution(message, run) {
591
+ const grant = message.execution_grant;
592
+ const claim = await this.executionDeliveryJournal.claim(grant.executionId);
593
+ if (claim === 'completed')
594
+ return;
595
+ if (claim === 'recovering') {
596
+ if (this.borgee.reportExecutionTelemetry) {
597
+ let reporter = this.executionTelemetryByExecutionId.get(grant.executionId);
598
+ const restoredReporter = !reporter;
599
+ reporter ??= this.createExecutionTelemetryReporter(grant);
600
+ this.executionTelemetryByExecutionId.set(grant.executionId, reporter);
601
+ await reporter.start();
602
+ if (restoredReporter) {
603
+ const persistedUsage = await this.executionDeliveryJournal.readUsage(grant.executionId);
604
+ if (persistedUsage) {
605
+ const { provider, ...usage } = persistedUsage;
606
+ await reporter.usage(provider, usage);
607
+ }
608
+ }
609
+ reporter.noteFailure();
610
+ let finishError;
611
+ try {
612
+ await reporter.finish(false);
613
+ }
614
+ catch (error) {
615
+ finishError = error;
616
+ }
617
+ if (reporter.allRequiredFramesDurable) {
618
+ await this.executionDeliveryJournal.complete(grant.executionId);
619
+ this.executionTelemetryByExecutionId.delete(grant.executionId);
620
+ }
621
+ if (finishError)
622
+ throw finishError;
623
+ return;
624
+ }
625
+ throw new Error('Execution telemetry is unavailable for interrupted delivery recovery');
626
+ }
627
+ const result = await this.runMessageWithExecutionTelemetry(message, run);
628
+ if (result.allRequiredFramesDurable) {
629
+ await this.executionDeliveryJournal.complete(grant.executionId);
630
+ }
631
+ if (result.error)
632
+ throw result.error;
633
+ }
634
+ async runMessageWithExecutionTelemetry(message, run) {
635
+ const grant = message.execution_grant;
636
+ const reporter = grant && this.borgee.reportExecutionTelemetry
637
+ ? this.executionTelemetryByExecutionId.get(grant.executionId) ??
638
+ this.createExecutionTelemetryReporter(grant)
639
+ : undefined;
640
+ if (grant && !reporter) {
641
+ return {
642
+ allRequiredFramesDurable: false,
643
+ error: new Error('Execution telemetry is unavailable for admitted delivery'),
644
+ };
645
+ }
646
+ if (grant && reporter) {
647
+ this.executionTelemetryByExecutionId.set(grant.executionId, reporter);
648
+ }
649
+ let runError;
650
+ try {
651
+ await run();
652
+ }
653
+ catch (error) {
654
+ reporter?.noteFailure();
655
+ runError = error;
656
+ }
657
+ let finishError;
658
+ if (reporter) {
659
+ try {
660
+ await reporter.finish();
661
+ }
662
+ catch (error) {
663
+ finishError = error;
664
+ this.logger.error('execution telemetry finish failed', {
665
+ error: summarizeError(error),
666
+ });
667
+ }
668
+ finally {
669
+ if (reporter.allRequiredFramesDurable &&
670
+ grant &&
671
+ this.executionTelemetryByExecutionId.get(grant.executionId) === reporter) {
672
+ this.executionTelemetryByExecutionId.delete(grant.executionId);
673
+ }
674
+ }
675
+ }
676
+ const error = runError && finishError
677
+ ? new AggregateError([runError, finishError], 'Execution and telemetry finalization both failed')
678
+ : runError ?? finishError;
679
+ return {
680
+ allRequiredFramesDurable: reporter?.allRequiredFramesDurable ?? grant === undefined,
681
+ ...(error ? { error } : {}),
682
+ };
683
+ }
684
+ createExecutionTelemetryReporter(grant) {
685
+ return new ExecutionTelemetryReporter(grant, {
686
+ reportExecutionTelemetry: (frame) => this.borgee.reportExecutionTelemetry(frame),
687
+ }, Date.now, (provider, usage) => this.executionDeliveryJournal.recordUsage(grant.executionId, {
688
+ provider,
689
+ ...usage,
690
+ }));
691
+ }
692
+ async handleSelectedSessionMessage(msg) {
693
+ if (!msg.sessionTarget || !msg.sourceCurrent?.())
694
+ return;
695
+ const normalized = await this.normalizeMessage(msg);
696
+ if (!normalized || !msg.sourceCurrent())
697
+ return;
698
+ await this.executeTurn(msg, normalized.authorId, normalized.content, {
699
+ incomingParts: normalized.incomingParts,
700
+ selectedSession: { ...msg.sessionTarget, sourceCurrent: msg.sourceCurrent },
701
+ });
702
+ }
484
703
  dispatchAutonomousReply(reply) {
485
704
  if (!this.started
486
705
  || this.controlPlaneClosed
@@ -2309,6 +2528,254 @@ export class AgentsHost {
2309
2528
  : {}),
2310
2529
  });
2311
2530
  }
2531
+ async handleSessionControl(request, context) {
2532
+ const response = (status, reason) => sessionControlResponse(request, status, reason);
2533
+ if (!this.started || this.controlPlaneClosed || context.signal.aborted || Date.now() >= request.deadline_at)
2534
+ return response('unconfirmed', 'session_control_expired');
2535
+ if (this.provider.outputMode !== 'normalized' || !this.provider.controlSession)
2536
+ return response('unsupported', 'session_control_not_supported');
2537
+ if (request.operation.kind === 'send_input')
2538
+ return this.handleNativeInput(request, context);
2539
+ if (this.borgee.sessionBindingsSupported)
2540
+ return this.handleRetainedControl(request, context);
2541
+ if (request.operation.kind === 'list_pending_inputs') {
2542
+ if (!this.provider.supportsSessionInput || !context.isConnectionCurrent)
2543
+ return response('unsupported', 'native_queue_inventory_not_supported');
2544
+ const connected = () => this.started && !this.controlPlaneClosed && this.selfAgentId === request.agent_id && context.isConnectionCurrent();
2545
+ if (!connected())
2546
+ return response('unconfirmed', 'session_control_expired');
2547
+ try {
2548
+ return (await this.provider.controlSession(request, { signal: context.signal, isConnectionCurrent: connected })).response;
2549
+ }
2550
+ catch {
2551
+ return response('unconfirmed', 'native_queue_inventory_unavailable');
2552
+ }
2553
+ }
2554
+ if (request.operation.kind === 'list_commands') {
2555
+ try {
2556
+ return (await this.provider.controlSession(request, { signal: context.signal })).response;
2557
+ }
2558
+ catch {
2559
+ return response('unconfirmed', 'command_directory_unavailable');
2560
+ }
2561
+ }
2562
+ const channelId = request.channel_id;
2563
+ const state = this.ensureCollaborationChannelState(channelId);
2564
+ if (state.processor || state.running || state.activeTurn || state.queue.length || this.channelTurnQueues.has(channelId))
2565
+ return response('rejected', 'session_busy');
2566
+ let finish;
2567
+ const ownership = new Promise(resolve => { finish = resolve; });
2568
+ state.processor = ownership;
2569
+ state.running = true;
2570
+ this.channelTurnQueues.set(channelId, ownership);
2571
+ let execution;
2572
+ let active;
2573
+ let retained = false;
2574
+ const release = () => {
2575
+ execution?.close();
2576
+ if (state.activeTurn === active)
2577
+ state.activeTurn = undefined;
2578
+ state.running = false;
2579
+ if (state.processor === ownership)
2580
+ state.processor = undefined;
2581
+ if (this.channelTurnQueues.get(channelId) === ownership)
2582
+ this.channelTurnQueues.delete(channelId);
2583
+ finish();
2584
+ };
2585
+ this.trackActiveTurn(ownership);
2586
+ try {
2587
+ if (request.operation.kind === 'execute_command') {
2588
+ if (!request.admission)
2589
+ return response('rejected', 'command_admission_required');
2590
+ const admission = request.admission;
2591
+ active = { generation: ++state.generation, turnExecutionId: admission.input_message_id, superseded: false, collaborationMessagesSent: 0, targetCooldowns: new Map() };
2592
+ state.activeTurn = active;
2593
+ const inputText = await readDurableInputText(this.borgee, { channelId, inputMessageId: admission.input_message_id, createdAt: admission.input_created_at });
2594
+ if (context.signal.aborted || Date.now() >= request.deadline_at)
2595
+ return response('unconfirmed', 'session_control_expired');
2596
+ execution = this.beginNormalizedExecution({ channelId, taskId: admission.input_message_id, inputMessageId: admission.input_message_id, inputText, activeTurn: active });
2597
+ }
2598
+ const result = await this.provider.controlSession(request, {
2599
+ signal: context.signal, execution: execution?.options,
2600
+ onCompletion: pending => {
2601
+ retained = true;
2602
+ const owner = active;
2603
+ const completion = pending.then(async (reply) => {
2604
+ if (!reply || !hasVisibleText(reply.text) || !(await this.canAcceptCompletedTurnOutput(channelId, owner)))
2605
+ return;
2606
+ await this.postTurnReply({ channelId, body: reply.text, inputMessageId: request.admission?.input_message_id, replyToId: request.admission?.input_message_id, taskId: request.admission.input_message_id, activeTurn: owner });
2607
+ }).catch(error => { this.logger.error('native command completion unavailable', { channelId, error: summarizeError(error) }); }).finally(release);
2608
+ this.trackActiveTurn(completion);
2609
+ },
2610
+ });
2611
+ return result.response;
2612
+ }
2613
+ catch (error) {
2614
+ this.logger.error('native session control unavailable', { channelId, error: summarizeError(error) });
2615
+ return response('unconfirmed', 'native_confirmation_unavailable');
2616
+ }
2617
+ finally {
2618
+ if (!retained)
2619
+ release();
2620
+ }
2621
+ }
2622
+ async handleRetainedControl(request, context) {
2623
+ const response = (status, reason) => sessionControlResponse(request, status, reason);
2624
+ const connected = () => this.started && !this.controlPlaneClosed && this.selfAgentId === request.agent_id && !!context.isConnectionCurrent?.();
2625
+ let execution;
2626
+ let canPublish = () => false;
2627
+ let retained = false;
2628
+ try {
2629
+ const admission = request.admission;
2630
+ let inputText = null;
2631
+ if (request.operation.kind === 'execute_command') {
2632
+ if (!admission)
2633
+ return response('rejected', 'command_admission_required');
2634
+ inputText = await readDurableInputText(this.borgee, { channelId: request.channel_id, inputMessageId: admission.input_message_id, createdAt: admission.input_created_at });
2635
+ }
2636
+ if (!connected() || context.signal.aborted || Date.now() >= request.deadline_at)
2637
+ return response('unconfirmed', 'session_control_expired');
2638
+ const result = await this.provider.controlSession(request, {
2639
+ signal: context.signal, connectionSignal: context.connectionSignal, isConnectionCurrent: connected,
2640
+ activateExecution: nativeCurrent => {
2641
+ if (!admission)
2642
+ throw new Error('Native command has no admitted input.');
2643
+ canPublish = () => connected() && nativeCurrent();
2644
+ execution = this.beginNormalizedExecution({ channelId: request.channel_id, taskId: admission.input_message_id, inputMessageId: admission.input_message_id, inputText, publicationGuard: canPublish });
2645
+ return execution.options;
2646
+ },
2647
+ onCompletion: pending => {
2648
+ retained = true;
2649
+ const completion = pending.then(async (reply) => {
2650
+ if (!reply || !hasVisibleText(reply.text) || !canPublish())
2651
+ return;
2652
+ await this.postTurnReply({ channelId: request.channel_id, body: reply.text, inputMessageId: admission.input_message_id, replyToId: admission.input_message_id, taskId: admission.input_message_id });
2653
+ }).catch(error => this.logger.error('native command completion unavailable', { channelId: request.channel_id, error: summarizeError(error) })).finally(() => execution?.close());
2654
+ this.trackActiveTurn(completion);
2655
+ },
2656
+ });
2657
+ return result.response;
2658
+ }
2659
+ catch (error) {
2660
+ this.logger.error('native session control unavailable', { channelId: request.channel_id, error: summarizeError(error) });
2661
+ return response('unconfirmed', 'native_confirmation_unavailable');
2662
+ }
2663
+ finally {
2664
+ if (!retained)
2665
+ execution?.close();
2666
+ }
2667
+ }
2668
+ async handleNativeInput(request, context) {
2669
+ const operation = request.operation;
2670
+ const admission = request.admission;
2671
+ const response = (status, reason) => sessionControlResponse(request, status, reason);
2672
+ if (operation.kind !== 'send_input' || !admission || !context.isConnectionCurrent || !this.provider.supportsSessionInput)
2673
+ return response('unsupported', 'native_input_not_supported');
2674
+ const connected = () => this.started && !this.controlPlaneClosed && this.selfAgentId === request.agent_id && context.isConnectionCurrent();
2675
+ let execution;
2676
+ let canPublish = () => false;
2677
+ let retained = false;
2678
+ try {
2679
+ const inputText = await readDurableInputText(this.borgee, { channelId: request.channel_id, inputMessageId: admission.input_message_id, createdAt: admission.input_created_at });
2680
+ if (inputText !== operation.text || !connected() || context.signal.aborted)
2681
+ return response('unconfirmed', 'native_input_admission_unavailable');
2682
+ const result = await this.provider.controlSession(request, {
2683
+ signal: context.signal, connectionSignal: context.connectionSignal, isConnectionCurrent: connected,
2684
+ activateExecution: async (nativeCurrent) => {
2685
+ canPublish = () => connected() && nativeCurrent();
2686
+ if (!canPublish())
2687
+ throw new Error('Native queued input connection ownership ended.');
2688
+ execution = this.beginNormalizedExecution({ channelId: request.channel_id, taskId: admission.input_message_id, inputMessageId: admission.input_message_id, inputText, publicationGuard: canPublish });
2689
+ return execution.options;
2690
+ },
2691
+ onCompletion: pending => {
2692
+ retained = true;
2693
+ const completion = pending.then(async (reply) => {
2694
+ if (!reply || !hasVisibleText(reply.text) || !canPublish())
2695
+ return;
2696
+ await this.postTurnReply({ channelId: request.channel_id, body: reply.text, inputMessageId: admission.input_message_id, replyToId: admission.input_message_id, taskId: admission.input_message_id });
2697
+ }).catch(error => this.logger.error('native input completion unavailable', { channelId: request.channel_id, error: summarizeError(error) })).finally(() => execution?.close());
2698
+ this.trackActiveTurn(completion);
2699
+ },
2700
+ });
2701
+ return result.response;
2702
+ }
2703
+ catch (error) {
2704
+ this.logger.error('native input confirmation unavailable', { channelId: request.channel_id, error: summarizeError(error) });
2705
+ return response('unconfirmed', 'native_confirmation_unavailable');
2706
+ }
2707
+ finally {
2708
+ if (!retained)
2709
+ execution?.close();
2710
+ }
2711
+ }
2712
+ async postTurnReply(input, source) {
2713
+ const message = { channelId: input.channelId, body: input.body, replyToId: input.replyToId ?? input.activeTurn?.delivery?.replyToId };
2714
+ const posted = source ? await this.borgee.postMessage(message, source) : await this.borgee.postMessage(message);
2715
+ recordProtocolTrace({ stage: 'host.delivery', kind: 'correlation', correlation: { channelId: input.channelId, agentId: this.selfAgentId ?? undefined, inputMessageId: input.inputMessageId, taskId: input.taskId, turnExecutionId: input.activeTurn?.turnExecutionId ?? undefined, outputMessageId: posted.messageId }, payload: { delivery: 'posted' } });
2716
+ return posted;
2717
+ }
2718
+ beginNormalizedExecution(input) {
2719
+ const { channelId, taskId, activeTurn } = input;
2720
+ let execution;
2721
+ try {
2722
+ execution = this.executionTelemetry?.bind({ bindingId: taskId, channelId, inputMessageId: input.inputMessageId, grant: input.grant });
2723
+ }
2724
+ catch (error) {
2725
+ this.logger.error('normalized accounting binding unavailable', { error: summarizeError(error) });
2726
+ }
2727
+ let publisher;
2728
+ let statePublisher;
2729
+ let stateClosed = false;
2730
+ const owner = Symbol('normalized-state');
2731
+ const stateOwnerKey = input.publicationGuard ? JSON.stringify([channelId, taskId]) : channelId;
2732
+ this.normalizedStateOwners.set(stateOwnerKey, owner);
2733
+ const canPublishState = () => {
2734
+ const current = this.collaborationChannels.get(channelId)?.activeTurn;
2735
+ return !stateClosed && this.started && !this.controlPlaneClosed && this.normalizedStateOwners.get(stateOwnerKey) === owner && (input.publicationGuard ? input.publicationGuard() : !activeTurn?.superseded && (!current || current.generation === activeTurn?.generation));
2736
+ };
2737
+ return {
2738
+ options: {
2739
+ normalizedTaskId: taskId, normalizedInputMessageId: input.inputMessageId, normalizedInputText: input.inputText,
2740
+ normalizedStateSink: input.silent ? undefined : {
2741
+ snapshot: (snapshot, bindingId, timelineEpoch) => {
2742
+ if (!canPublishState())
2743
+ return false;
2744
+ statePublisher ??= new NormalizedActivityPublisher({ client: { reportActivity: value => { this.borgee.reportActivity?.(value); } }, channelId, taskId, agentId: snapshot.payload.id });
2745
+ statePublisher.publish(snapshot, Date.now(), snapshot.payload.providerId, snapshot.payload.capabilities, bindingId, timelineEpoch);
2746
+ return true;
2747
+ },
2748
+ sessionActivity: activity => { if (!canPublishState() || !this.borgee.reportActivity)
2749
+ return false; this.borgee.reportActivity({ channelId, activity }); return true; },
2750
+ observe: (observation, agentId, capabilities, bindingId, timelineEpoch) => {
2751
+ if (observation.event.type !== 'runtime_updated' || !canPublishState() || this.borgee.sessionBindingsSupported)
2752
+ return;
2753
+ statePublisher ??= new NormalizedActivityPublisher({ client: { reportActivity: value => { this.borgee.reportActivity?.(value); } }, channelId, taskId, agentId });
2754
+ statePublisher.observe(observation, capabilities, bindingId, timelineEpoch);
2755
+ },
2756
+ close: () => { stateClosed = true; statePublisher?.close(); if (this.normalizedStateOwners.get(stateOwnerKey) === owner)
2757
+ this.normalizedStateOwners.delete(stateOwnerKey); },
2758
+ },
2759
+ onNormalizedObservation: (event, agentId, observation, capabilities, bindingId, timelineEpoch, sessionPublished) => {
2760
+ if (input.publicationGuard && !input.publicationGuard())
2761
+ return;
2762
+ try {
2763
+ execution?.observe(event, observation?.occurredAt);
2764
+ }
2765
+ catch (error) {
2766
+ this.logger.error('normalized accounting rejected', { error: summarizeError(error) });
2767
+ }
2768
+ const acceptsOutput = !input.silent && this.canAcceptTurnOutput(channelId, activeTurn?.generation);
2769
+ recordProtocolTrace({ stage: 'host.dispatch', kind: 'correlation', correlation: { channelId, agentId, inputMessageId: input.inputMessageId, taskId, turnExecutionId: activeTurn?.turnExecutionId ?? undefined }, payload: { eventType: event.type }, outcome: acceptsOutput ? 'forwarded' : 'suppressed' });
2770
+ if (sessionPublished || !acceptsOutput || event.type === 'runtime_updated' && this.borgee.sessionBindingsSupported)
2771
+ return;
2772
+ publisher ??= new NormalizedActivityPublisher({ client: { reportActivity: value => { this.borgee.reportActivity?.(value); } }, channelId, taskId, agentId });
2773
+ publisher.observe(observation ?? event, capabilities, bindingId, timelineEpoch);
2774
+ },
2775
+ },
2776
+ close: () => { publisher?.close(); execution?.close(); },
2777
+ };
2778
+ }
2312
2779
  async executeTurn(msg, authorId, content, options) {
2313
2780
  const channelId = msg.channel_id;
2314
2781
  const activeTurn = options?.activeTurn;
@@ -2316,23 +2783,14 @@ export class AgentsHost {
2316
2783
  const taskThreadContext = options?.taskThreadContext ?? { state: 'inactive' };
2317
2784
  const silentTurn = options?.silent === true;
2318
2785
  const deferDelivery = options?.deferDelivery === true;
2786
+ const selectedSourceCurrent = options?.selectedSession?.sourceCurrent;
2787
+ const selectedWriteSource = selectedSourceCurrent ? { isConnectionCurrent: selectedSourceCurrent } : undefined;
2319
2788
  const collaborationTurnMode = activeTurn?.protocol
2320
2789
  ? 'protocol-managed'
2321
2790
  : options?.kickoff
2322
2791
  ? 'silent-kickoff'
2323
2792
  : undefined;
2324
2793
  const turnExecutionId = activeTurn?.turnExecutionId ?? null;
2325
- const stopTyping = silentTurn ? () => { } : this.borgee.startTyping(channelId);
2326
- const collaborationGrounding = await this.buildCollaborationGrounding(authorId, options?.incomingAuthorKind, activeTurn, options?.kickoff);
2327
- const publishesCollaborationDraft = !silentTurn &&
2328
- !deferDelivery &&
2329
- this.collaborationEnabled &&
2330
- turnExecutionId != null &&
2331
- activeTurn?.protocol === undefined;
2332
- const supportsProviderProgress = this.provider.capabilities?.progressUpdates?.support === 'supported';
2333
- const supportsBackgroundRunLifecycle = this.provider.capabilities?.backgroundRuns?.lifecycle === 'supported';
2334
- const reportsProviderActivity = !silentTurn && supportsProviderProgress;
2335
- const observeProviderProgress = supportsProviderProgress || supportsBackgroundRunLifecycle;
2336
2794
  const turnContext = {
2337
2795
  eventType: msg.type,
2338
2796
  channelId,
@@ -2345,26 +2803,64 @@ export class AgentsHost {
2345
2803
  };
2346
2804
  const turnStartedAt = Date.now();
2347
2805
  this.logger.debug('starting turn', turnContext);
2806
+ const executionTelemetry = this.provider.outputMode !== 'normalized' && msg.execution_grant
2807
+ ? this.executionTelemetryByExecutionId.get(msg.execution_grant.executionId)
2808
+ : undefined;
2809
+ let executionSucceeded = false;
2810
+ let executionTelemetryStartFailed = false;
2348
2811
  let reply;
2349
2812
  let publicReplyText;
2350
2813
  let visibleReplyBody;
2351
- const taskThreadCollaborationContract = this.resolveTaskThreadCollaborationContractForTurn(msg, taskThreadContext);
2352
- const collaborationOutcome = this.collaborationOutcomeModelEnabled
2353
- ? this.collaborationOutcomeByChannel.get(channelId)
2354
- : undefined;
2355
- const missedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(channelId);
2356
- const collaborationCapabilities = this.resolveCollaborationCapabilityDeclarationForTurn(taskThreadContext);
2357
- const incomingParts = options?.incomingParts
2358
- ?? buildHostedTurnContentParts({
2359
- text: String(msg.content ?? msg.body ?? content),
2360
- attachments: msg.attachments,
2361
- });
2362
- const activityReporter = silentTurn
2363
- ? undefined
2364
- : this.beginTurnActivity(channelId, activeTurn?.generation);
2365
- await this.ensureCompactionSnapshotLoaded(channelId);
2366
- const compactionSnapshot = this.compactionSnapshotByChannel.get(channelId);
2814
+ let activityReporter;
2815
+ let typingLease;
2816
+ const normalizedTaskId = turnExecutionId ?? randomUUID();
2817
+ recordProtocolTrace({ stage: 'host.dispatch', kind: 'correlation',
2818
+ correlation: { channelId, agentId: this.selfAgentId ?? undefined, inputMessageId: msg.message_id, taskId: normalizedTaskId, turnExecutionId: turnExecutionId ?? undefined },
2819
+ payload: { silentTurn } });
2820
+ let normalizedExecution;
2367
2821
  try {
2822
+ if (!silentTurn) {
2823
+ typingLease = startTypingLease(() => this.borgee.reportTyping(channelId));
2824
+ }
2825
+ const collaborationGrounding = await this.buildCollaborationGrounding(authorId, options?.incomingAuthorKind, activeTurn, options?.kickoff);
2826
+ const publishesCollaborationDraft = !silentTurn &&
2827
+ !deferDelivery &&
2828
+ this.collaborationEnabled &&
2829
+ turnExecutionId != null &&
2830
+ activeTurn?.protocol === undefined;
2831
+ const supportsProviderProgress = this.provider.capabilities?.progressUpdates?.support === 'supported';
2832
+ const supportsBackgroundRunLifecycle = this.provider.capabilities?.backgroundRuns?.lifecycle === 'supported';
2833
+ const reportsProviderActivity = !silentTurn && supportsProviderProgress;
2834
+ const observeProviderProgress = supportsProviderProgress || supportsBackgroundRunLifecycle;
2835
+ const taskThreadCollaborationContract = this.resolveTaskThreadCollaborationContractForTurn(msg, taskThreadContext);
2836
+ const collaborationOutcome = this.collaborationOutcomeModelEnabled
2837
+ ? this.collaborationOutcomeByChannel.get(channelId)
2838
+ : undefined;
2839
+ const missedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(channelId);
2840
+ const collaborationCapabilities = this.resolveCollaborationCapabilityDeclarationForTurn(taskThreadContext);
2841
+ const incomingParts = options?.incomingParts
2842
+ ?? buildHostedTurnContentParts({
2843
+ text: String(msg.content ?? msg.body ?? content),
2844
+ attachments: msg.attachments,
2845
+ });
2846
+ activityReporter = silentTurn
2847
+ ? undefined
2848
+ : this.beginTurnActivity(channelId, activeTurn?.generation);
2849
+ normalizedExecution = this.provider.outputMode === 'normalized' ? this.beginNormalizedExecution({ channelId, taskId: normalizedTaskId, inputMessageId: msg.message_id, inputText: await readNormalizedInputText(this.borgee, msg), grant: msg.execution_grant, activeTurn, silent: silentTurn, ...(selectedSourceCurrent ? { publicationGuard: selectedSourceCurrent } : {}) }) : undefined;
2850
+ await this.ensureCompactionSnapshotLoaded(channelId);
2851
+ const compactionSnapshot = this.compactionSnapshotByChannel.get(channelId);
2852
+ if (executionTelemetry) {
2853
+ try {
2854
+ await executionTelemetry.start();
2855
+ }
2856
+ catch (error) {
2857
+ executionTelemetryStartFailed = true;
2858
+ this.logger.error('execution telemetry start failed', {
2859
+ error: summarizeError(error),
2860
+ });
2861
+ throw error;
2862
+ }
2863
+ }
2368
2864
  reply = await this.provider.generateReply({
2369
2865
  agentName: this.config.agent.agentName,
2370
2866
  provider: this.config.agent.provider,
@@ -2374,6 +2870,7 @@ export class AgentsHost {
2374
2870
  incomingParts,
2375
2871
  incomingEventKind: msg.type,
2376
2872
  incomingMessageType: msg.message_type,
2873
+ ...(options?.selectedSession ? { selectedSession: options.selectedSession } : {}),
2377
2874
  collaboration: turnExecutionId || options?.kickoff
2378
2875
  ? {
2379
2876
  enabled: this.collaborationEnabled,
@@ -2396,7 +2893,8 @@ export class AgentsHost {
2396
2893
  ? {
2397
2894
  taskAssignmentContextOverride: {
2398
2895
  active: true,
2399
- ...(taskThreadContext.state === 'active' && taskThreadContext.currentTaskId
2896
+ ...((taskThreadContext.state === 'active'
2897
+ || taskThreadContext.state === 'recovered') && taskThreadContext.currentTaskId
2400
2898
  ? { currentTaskId: taskThreadContext.currentTaskId }
2401
2899
  : {}),
2402
2900
  },
@@ -2405,42 +2903,62 @@ export class AgentsHost {
2405
2903
  : { taskAssignmentContextOverridePersistence: 'ephemeral' }),
2406
2904
  }
2407
2905
  : {}),
2408
- }, observeProviderProgress
2906
+ }, observeProviderProgress || executionTelemetry || this.provider.outputMode === 'normalized'
2409
2907
  ? {
2410
- onProgress: (update) => {
2411
- if (supportsBackgroundRunLifecycle) {
2412
- this.backgroundRuns.observe(channelId, update);
2413
- if (update.type === 'activity' && update.activity.background === true) {
2414
- this.borgee.reportActivity?.({
2415
- channelId,
2416
- activity: { shape: 'activity', ...update.activity },
2417
- });
2418
- return;
2419
- }
2420
- }
2421
- if (!this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
2422
- return;
2423
- }
2424
- if (supportsProviderProgress && update.type === 'compaction') {
2425
- this.noteCompactionProgress(channelId, update.compaction, {
2426
- turnExecutionId: activeTurn?.turnExecutionId ?? undefined,
2427
- });
2428
- return;
2908
+ ...normalizedExecution?.options,
2909
+ ...(executionTelemetry
2910
+ ? {
2911
+ onUsage: (usage) => {
2912
+ void executionTelemetry.usage(this.config.agent.provider, usage);
2913
+ },
2914
+ onExecutionContinuation: (completion) => {
2915
+ executionTelemetry.hold(completion);
2916
+ },
2429
2917
  }
2430
- const reportedActivity = toReportedActivity(update);
2431
- if (reportsProviderActivity && reportedActivity) {
2432
- activityReporter?.progress(reportedActivity);
2433
- }
2434
- if (!supportsProviderProgress
2435
- || update.type !== 'text'
2436
- || update.stream !== 'answer') {
2437
- return;
2438
- }
2439
- if (!publishesCollaborationDraft) {
2440
- return;
2918
+ : {}),
2919
+ ...(observeProviderProgress
2920
+ ? {
2921
+ onProgress: (update) => {
2922
+ if (supportsBackgroundRunLifecycle) {
2923
+ this.backgroundRuns.observe(channelId, update);
2924
+ if (update.type === 'activity' && update.activity.background === true) {
2925
+ this.borgee.reportActivity?.({
2926
+ channelId,
2927
+ activity: { shape: 'activity', ...update.activity },
2928
+ });
2929
+ return;
2930
+ }
2931
+ }
2932
+ if ((selectedSourceCurrent && !selectedSourceCurrent()) || !this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
2933
+ return;
2934
+ }
2935
+ if (update.type === 'text'
2936
+ && update.stream === 'answer'
2937
+ && hasVisibleText(update.text)) {
2938
+ typingLease?.renew();
2939
+ }
2940
+ if (supportsProviderProgress && update.type === 'compaction') {
2941
+ this.noteCompactionProgress(channelId, update.compaction, {
2942
+ turnExecutionId: activeTurn?.turnExecutionId ?? undefined,
2943
+ });
2944
+ return;
2945
+ }
2946
+ const reportedActivity = toReportedActivity(update);
2947
+ if (this.provider.outputMode !== 'normalized' && reportsProviderActivity && reportedActivity) {
2948
+ activityReporter?.progress(reportedActivity);
2949
+ }
2950
+ if (!supportsProviderProgress ||
2951
+ update.type !== 'text' ||
2952
+ update.stream !== 'answer') {
2953
+ return;
2954
+ }
2955
+ if (!publishesCollaborationDraft) {
2956
+ return;
2957
+ }
2958
+ this.publishCollaborationDraft(channelId, activeTurn, update.text);
2959
+ },
2441
2960
  }
2442
- this.publishCollaborationDraft(channelId, activeTurn, update.text);
2443
- },
2961
+ : {}),
2444
2962
  }
2445
2963
  : undefined);
2446
2964
  publicReplyText = resolvePublicReplyText(reply);
@@ -2458,7 +2976,7 @@ export class AgentsHost {
2458
2976
  observedAt: Date.now(),
2459
2977
  turnExecutionId: activeTurn?.turnExecutionId ?? undefined,
2460
2978
  });
2461
- if (!(await this.canAcceptCompletedTurnOutput(channelId, activeTurn))) {
2979
+ if ((selectedSourceCurrent && !selectedSourceCurrent()) || !(await this.canAcceptCompletedTurnOutput(channelId, activeTurn))) {
2462
2980
  if (activeTurn?.turnExecutionId) {
2463
2981
  this.clearCollaborationDraft(channelId, {
2464
2982
  generation: activeTurn.generation,
@@ -2468,6 +2986,7 @@ export class AgentsHost {
2468
2986
  return { reply, stale: true, completed: false };
2469
2987
  }
2470
2988
  if (silentTurn) {
2989
+ executionSucceeded = true;
2471
2990
  this.logger.debug('completed turn', {
2472
2991
  ...turnContext,
2473
2992
  durationMs: Date.now() - turnStartedAt,
@@ -2488,6 +3007,7 @@ export class AgentsHost {
2488
3007
  };
2489
3008
  }
2490
3009
  if (deferDelivery) {
3010
+ executionSucceeded = true;
2491
3011
  this.logger.debug('completed turn', {
2492
3012
  ...turnContext,
2493
3013
  durationMs: Date.now() - turnStartedAt,
@@ -2508,11 +3028,7 @@ export class AgentsHost {
2508
3028
  };
2509
3029
  }
2510
3030
  if (this.started && !this.controlPlaneClosed && hasVisibleText(publicReplyText)) {
2511
- const posted = await this.borgee.postMessage({
2512
- channelId,
2513
- body: visibleReplyBody,
2514
- replyToId: activeTurn?.delivery?.replyToId,
2515
- });
3031
+ const posted = await this.postTurnReply({ channelId, body: visibleReplyBody, inputMessageId: msg.message_id, taskId: normalizedTaskId, activeTurn }, selectedWriteSource);
2516
3032
  this.logger.debug('completed turn', {
2517
3033
  ...turnContext,
2518
3034
  durationMs: Date.now() - turnStartedAt,
@@ -2524,6 +3040,7 @@ export class AgentsHost {
2524
3040
  if (publishesCollaborationDraft) {
2525
3041
  this.finalizeCollaborationDraft(channelId, activeTurn, posted.messageId, visibleReplyBody);
2526
3042
  }
3043
+ executionSucceeded = true;
2527
3044
  return {
2528
3045
  reply,
2529
3046
  deliveredMessageId: posted.messageId,
@@ -2547,6 +3064,7 @@ export class AgentsHost {
2547
3064
  turnExecutionId: activeTurn.turnExecutionId,
2548
3065
  });
2549
3066
  }
3067
+ executionSucceeded = true;
2550
3068
  return {
2551
3069
  reply,
2552
3070
  deliveredMessageId: null,
@@ -2565,6 +3083,13 @@ export class AgentsHost {
2565
3083
  turnExecutionId: activeTurn.turnExecutionId,
2566
3084
  });
2567
3085
  }
3086
+ if (executionTelemetryStartFailed) {
3087
+ throw error;
3088
+ }
3089
+ if (error instanceof ProviderInputRejectedError && !silentTurn && !deferDelivery && (!selectedSourceCurrent || selectedSourceCurrent()) && this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
3090
+ const posted = await this.postTurnReply({ channelId, body: error.publicReplyText, replyToId: activeTurn?.delivery?.replyToId, inputMessageId: msg.message_id, taskId: normalizedTaskId, activeTurn }, selectedWriteSource);
3091
+ return { reply: { text: error.publicReplyText }, publicReplyText: error.publicReplyText, visibleReplyBody: error.publicReplyText, deliveredMessageId: posted.messageId, deliveryState: 'posted', stale: false, completed: false };
3092
+ }
2568
3093
  if (error instanceof ProviderTurnCancelledError) {
2569
3094
  const cancelledReplyText = error.publicReplyText?.trim();
2570
3095
  if (!silentTurn &&
@@ -2572,16 +3097,13 @@ export class AgentsHost {
2572
3097
  typeof cancelledReplyText === 'string' &&
2573
3098
  cancelledReplyText.length > 0 &&
2574
3099
  this.started &&
2575
- !this.controlPlaneClosed) {
3100
+ !this.controlPlaneClosed &&
3101
+ (!selectedSourceCurrent || selectedSourceCurrent())) {
2576
3102
  reply = { text: cancelledReplyText };
2577
3103
  publicReplyText = cancelledReplyText;
2578
3104
  visibleReplyBody = this.resolveVisibleTurnBody(activeTurn, reply, publicReplyText);
2579
3105
  try {
2580
- const posted = await this.borgee.postMessage({
2581
- channelId,
2582
- body: visibleReplyBody,
2583
- replyToId: activeTurn?.delivery?.replyToId,
2584
- });
3106
+ const posted = await this.postTurnReply({ channelId, body: visibleReplyBody, replyToId: activeTurn?.delivery?.replyToId, inputMessageId: msg.message_id, taskId: normalizedTaskId, activeTurn }, selectedWriteSource);
2585
3107
  this.logger.debug('posted cancelled turn feedback', {
2586
3108
  ...turnContext,
2587
3109
  durationMs: Date.now() - turnStartedAt,
@@ -2642,8 +3164,12 @@ export class AgentsHost {
2642
3164
  };
2643
3165
  }
2644
3166
  finally {
2645
- stopTyping();
3167
+ if (executionTelemetry) {
3168
+ executionTelemetry.noteAttempt(executionSucceeded);
3169
+ }
3170
+ typingLease?.stop();
2646
3171
  activityReporter?.end();
3172
+ normalizedExecution?.close();
2647
3173
  }
2648
3174
  }
2649
3175
  /**