Skip to content

Commit 9be20b1

Browse files
RoyLinRoyLin
authored andcommitted
feat: integrate AHP harness into SDK and core (v1.2.2)
- core: add `hook_executor` field to `SessionOptions` and `with_hook_executor()` builder - core: `AgentSession` stores `ahp_executor`; `build_agent_loop()` prefers it over built-in `HookEngine` - sdk/python: add `HarnessServer` class and `harness_server` field on `SessionOptions`; dispatch to `AhpHookExecutor::spawn()` in `build_rust_session_options()` - sdk/node: add `HarnessServer` class and `harnessServer` field on `SessionOptions`; dispatch to `AhpHookExecutor::spawn()` in `js_session_options_to_rust()` - both SDKs: depend on `a3s-ahp` crate for `AhpHookExecutor`
1 parent 6842bfb commit 9be20b1

5 files changed

Lines changed: 103 additions & 2 deletions

File tree

core/src/agent_api.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ pub struct SessionOptions {
166166
/// Users can customize role, guidelines, response style, and extra instructions
167167
/// without losing the core agentic capabilities.
168168
pub prompt_slots: Option<SystemPromptSlots>,
169+
/// Optional external hook executor (e.g. an AHP harness server).
170+
///
171+
/// When set, **replaces** the built-in `HookEngine` for this session.
172+
/// All 11 lifecycle events are forwarded to the executor instead of being
173+
/// dispatched locally. The executor is also propagated to sub-agents via
174+
/// the sentinel hook mechanism.
175+
pub hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
169176
}
170177

171178
impl std::fmt::Debug for SessionOptions {
@@ -528,6 +535,19 @@ impl SessionOptions {
528535
self.prompt_slots = Some(slots);
529536
self
530537
}
538+
539+
/// Replace the built-in hook engine with an external hook executor.
540+
///
541+
/// Use this to attach an AHP harness server (or any custom `HookExecutor`)
542+
/// to the session. All lifecycle events will be forwarded to the executor
543+
/// instead of the in-process `HookEngine`.
544+
pub fn with_hook_executor(
545+
mut self,
546+
executor: Arc<dyn crate::hooks::HookExecutor>,
547+
) -> Self {
548+
self.hook_executor = Some(executor);
549+
self
550+
}
531551
}
532552

533553
// ============================================================================
@@ -1229,6 +1249,7 @@ impl Agent {
12291249
session_store,
12301250
auto_save: opts.auto_save,
12311251
hook_engine: Arc::new(crate::hooks::HookEngine::new()),
1252+
ahp_executor: opts.hook_executor.clone(),
12321253
init_warning,
12331254
command_registry: std::sync::Mutex::new(command_registry),
12341255
model_name: opts
@@ -1277,6 +1298,9 @@ pub struct AgentSession {
12771298
auto_save: bool,
12781299
/// Hook engine for lifecycle event interception.
12791300
hook_engine: Arc<crate::hooks::HookEngine>,
1301+
/// Optional external hook executor (e.g. AHP harness). When set, replaces
1302+
/// `hook_engine` as the executor passed to each `AgentLoop`.
1303+
ahp_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
12801304
/// Deferred init warning: emitted as PersistenceFailed on first send() if set.
12811305
init_warning: Option<String>,
12821306
/// Slash command registry for `/command` dispatch.
@@ -1314,8 +1338,13 @@ impl AgentSession {
13141338
/// Propagates the lane queue (if configured) for external task handling.
13151339
fn build_agent_loop(&self) -> AgentLoop {
13161340
let mut config = self.config.clone();
1317-
config.hook_engine =
1318-
Some(Arc::clone(&self.hook_engine) as Arc<dyn crate::hooks::HookExecutor>);
1341+
config.hook_engine = Some(
1342+
if let Some(ref ahp) = self.ahp_executor {
1343+
ahp.clone()
1344+
} else {
1345+
Arc::clone(&self.hook_engine) as Arc<dyn crate::hooks::HookExecutor>
1346+
},
1347+
);
13191348
// Always use live tool definitions so tools added via add_mcp_server() are visible
13201349
// to the LLM. The config.tools snapshot taken at session creation misses dynamically
13211350
// added MCP tools.

sdk/node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ crate-type = ["cdylib"]
1212

1313
[dependencies]
1414
a3s-code-core = { version = "1.0", path = "../../core" }
15+
a3s-ahp = { version = "0.1", path = "../../../ahp" }
1516
napi = { version = "2", features = ["async", "napi6", "serde-json"] }
1617
napi-derive = "2"
1718
tokio = { version = "1.35", features = ["full"] }

sdk/node/src/lib.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,40 @@ impl DefaultSecurityProvider {
414414
}
415415
}
416416

417+
/// Plain shim for `HarnessServer` — used as the `SessionOptions.harnessServer` field type.
418+
#[napi(object)]
419+
#[derive(Clone, Default)]
420+
pub struct JsHarnessServer {
421+
pub program: String,
422+
pub args: Vec<String>,
423+
}
424+
425+
/// External AHP harness server configuration.
426+
///
427+
/// Pass to `SessionOptions.harnessServer` to attach an external harness process
428+
/// that supervises every tool call and prompt in this session via JSON-RPC 2.0.
429+
///
430+
/// ```js
431+
/// agent.session('.', { harnessServer: new HarnessServer('python3', ['harness.py']) });
432+
/// ```
433+
#[napi]
434+
pub struct HarnessServer {
435+
pub program: String,
436+
pub args: Vec<String>,
437+
}
438+
439+
#[napi]
440+
impl HarnessServer {
441+
/// Create a harness server config.
442+
///
443+
/// @param program - Executable to launch (e.g. "python3")
444+
/// @param args - Arguments passed to the executable (e.g. ["harness.py"])
445+
#[napi(constructor)]
446+
pub fn new(program: String, args: Vec<String>) -> Self {
447+
Self { program, args }
448+
}
449+
}
450+
417451
// ============================================================================
418452
// SessionOptions
419453
// ============================================================================
@@ -500,6 +534,15 @@ pub struct SessionOptions {
500534
pub session_id: Option<String>,
501535
/// Automatically save the session to the configured store after each turn (default: false).
502536
pub auto_save: Option<bool>,
537+
/// External AHP harness server.
538+
///
539+
/// Pass `new HarnessServer("python3", ["harness.py"])` to attach an external
540+
/// supervision process. The harness receives every `pre_tool_use` and `pre_prompt`
541+
/// event and can return `block`, `skip`, `retry`, or `continue`.
542+
/// ```js
543+
/// agent.session('.', { harnessServer: new HarnessServer('python3', ['harness.py']) });
544+
/// ```
545+
pub harness_server: Option<JsHarnessServer>,
503546
}
504547

505548
/// A single message in conversation history.
@@ -800,6 +843,19 @@ fn js_session_options_to_rust(options: Option<SessionOptions>) -> RustSessionOpt
800843
if o.auto_save.unwrap_or(false) {
801844
opts = opts.with_auto_save(true);
802845
}
846+
if let Some(hs) = o.harness_server {
847+
match get_runtime().block_on(a3s_ahp::AhpHookExecutor::spawn(
848+
&hs.program,
849+
hs.args.as_slice(),
850+
)) {
851+
Ok(executor) => {
852+
opts = opts.with_hook_executor(executor);
853+
}
854+
Err(e) => {
855+
eprintln!("a3s-code: AHP harness spawn failed: {e}");
856+
}
857+
}
858+
}
803859
opts
804860
}
805861

sdk/python/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ crate-type = ["cdylib"]
1313

1414
[dependencies]
1515
a3s-code-core = { version = "1.0", path = "../../core" }
16+
a3s-ahp = { version = "0.1", path = "../../../ahp" }
1617
pyo3 = { version = "0.23", features = ["extension-module"] }
1718
tokio = { version = "1.35", features = ["full"] }
1819
serde_json = "1.0"

sdk/python/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2795,6 +2795,19 @@ fn build_rust_session_options(so: PySessionOptions) -> RustSessionOptions {
27952795
if so.auto_save {
27962796
o = o.with_auto_save(true);
27972797
}
2798+
if let Some(hs) = so.harness_server {
2799+
match get_runtime().block_on(a3s_ahp::AhpHookExecutor::spawn(
2800+
&hs.program,
2801+
hs.args.as_slice(),
2802+
)) {
2803+
Ok(executor) => {
2804+
o = o.with_hook_executor(executor);
2805+
}
2806+
Err(e) => {
2807+
eprintln!("a3s-code: AHP harness spawn failed: {e}");
2808+
}
2809+
}
2810+
}
27982811
o
27992812
}
28002813

@@ -4507,6 +4520,7 @@ fn a3s_code(m: &Bound<'_, PyModule>) -> PyResult<()> {
45074520
m.add_class::<PyFileSessionStore>()?;
45084521
m.add_class::<PyMemorySessionStore>()?;
45094522
m.add_class::<PyDefaultSecurityProvider>()?;
4523+
m.add_class::<PyHarnessServer>()?;
45104524
m.add_class::<PySessionOptions>()?;
45114525
m.add_class::<PySessionQueueConfig>()?;
45124526
m.add_class::<PySearchConfig>()?;

0 commit comments

Comments
 (0)