1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use std::{
collections::BTreeMap,
future::Future,
pin::Pin,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use futures_core::Stream;
use tokio::sync::Notify;
use crate::{
AgentWrapperCompletion, AgentWrapperError, AgentWrapperEvent, AgentWrapperKind,
AgentWrapperRunRequest,
};
pub(crate) type DynBackendEventStream<E, BE> =
Pin<Box<dyn Stream<Item = Result<E, BE>> + Send + 'static>>;
pub(crate) type DynBackendCompletionFuture<C, BE> =
Pin<Box<dyn Future<Output = Result<C, BE>> + Send + 'static>>;
#[derive(Clone, Debug, Default)]
pub(crate) struct EventObservabilitySignal {
inner: Arc<EventObservabilitySignalInner>,
}
#[derive(Debug, Default)]
struct EventObservabilitySignalInner {
done: AtomicBool,
notify: Notify,
}
impl EventObservabilitySignal {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn signal(&self) {
if self.inner.done.swap(true, Ordering::SeqCst) {
return;
}
self.inner.notify.notify_waiters();
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum BackendHarnessErrorPhase {
Spawn,
Stream,
Completion,
}
pub(crate) struct BackendSpawn<E, C, BE> {
pub events: DynBackendEventStream<E, BE>,
pub completion: DynBackendCompletionFuture<C, BE>,
pub events_observability: Option<EventObservabilitySignal>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct BackendDefaults {
pub env: BTreeMap<String, String>,
pub default_timeout: Option<Duration>,
}
pub(crate) struct NormalizedRequest<P> {
/// Stable identity for error reporting and event stamping.
pub agent_kind: AgentWrapperKind,
/// Preserved from `AgentWrapperRunRequest` (must be non-empty after trimming).
pub prompt: String,
/// Typed handoff for the shared model-selection field after normalization.
pub model_id: Option<String>,
/// Preserved from `AgentWrapperRunRequest` (no harness defaulting in v1).
pub working_dir: Option<std::path::PathBuf>,
/// Derived per BH-C03. `Some(Duration::ZERO)` is an explicit “no timeout” request.
pub effective_timeout: Option<Duration>,
/// Derived per BH-C03: `defaults.env` overridden by `request.env`.
pub env: BTreeMap<String, String>,
/// Backend-owned extracted policy derived from `request.extensions` after the allowlist check.
pub policy: P,
}
pub(crate) trait BackendHarnessAdapter: Send + Sync + 'static {
/// MUST return a stable, lower_snake_case id (see `AgentWrapperKind` rules).
fn kind(&self) -> AgentWrapperKind;
/// Supported extension keys for this backend (exact string match; case-sensitive).
///
/// This list MUST include both:
/// - core keys under `agent_api.*` that the backend supports, and
/// - backend keys under `backend.<agent_kind>.*` owned by the backend.
fn supported_extension_keys(&self) -> &'static [&'static str];
/// Backend-owned policy extracted from known extension keys only.
///
/// This hook MUST NOT implement “unknown key” rejection (that is BH-C02, harness-owned).
type Policy: Send + 'static;
fn validate_and_extract_policy(
&self,
request: &AgentWrapperRunRequest,
) -> Result<Self::Policy, AgentWrapperError>;
/// Typed backend event and completion types emitted by the wrapper runtime.
type BackendEvent: Send + 'static;
type BackendCompletion: Send + 'static;
/// Backend error type used at spawn/stream/completion boundaries.
type BackendError: Send + Sync + 'static;
/// Spawns the backend run using only the normalized request.
///
/// The returned stream MUST be drained to completion by the harness pump (BH-C04).
#[allow(clippy::type_complexity)]
fn spawn(
&self,
req: NormalizedRequest<Self::Policy>,
) -> Pin<
Box<
dyn Future<
Output = Result<
BackendSpawn<
Self::BackendEvent,
Self::BackendCompletion,
Self::BackendError,
>,
Self::BackendError,
>,
> + Send
+ 'static,
>,
>;
/// Maps one typed backend event into 0..N universal events.
///
/// Mapping is **infallible** by contract: backends MUST convert parse errors into
/// `BackendError` at the stream boundary, not here.
fn map_event(&self, event: Self::BackendEvent) -> Vec<AgentWrapperEvent>;
/// Maps a typed backend completion value to the universal completion payload.
fn map_completion(
&self,
completion: Self::BackendCompletion,
) -> Result<AgentWrapperCompletion, AgentWrapperError>;
/// Produces a safe/redacted message for a backend error at a given phase.
///
/// This message MUST NOT contain raw backend stdout/stderr lines or raw JSONL lines.
/// It MAY include bounded metadata such as `line_bytes=<n>` or a stable error kind tag.
fn redact_error(&self, phase: BackendHarnessErrorPhase, err: &Self::BackendError) -> String;
}
#[cfg(test)]
mod tests;