@inneranimalmedia/agentsam-sdk 2.6.8 → 2.6.10

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 (203) hide show
  1. package/README.md +18 -0
  2. package/apps/README.md +25 -0
  3. package/apps/cad-creator/shared/cad/package.json +1 -1
  4. package/apps/client-cms-editor/backend/package.json +5 -7
  5. package/apps/client-cms-editor/frontend/package.json +5 -7
  6. package/apps/client-cms-editor/frontend/tsconfig.json +27 -8
  7. package/apps/client-cms-editor/package-lock.json +8 -8
  8. package/apps/client-cms-editor/package.json +1 -1
  9. package/apps/client-cms-editor/shared/cms/package.json +3 -5
  10. package/docs/LOCAL_ENGINE_COMPUTE_AUDIT_PLAN.md +299 -0
  11. package/docs/architecture/HOOKS-MODELS-SELF-HOSTING-PLAN.md +199 -0
  12. package/examples/hyperspace-demo/index.html +16 -0
  13. package/examples/hyperspace-demo/package.json +21 -0
  14. package/examples/hyperspace-demo/src/main.tsx +160 -0
  15. package/examples/hyperspace-demo/tsconfig.json +9 -0
  16. package/examples/hyperspace-demo/vite.config.ts +24 -0
  17. package/migrations/runtime/0005_agentsam_hooks.sql +87 -0
  18. package/package.json +37 -10
  19. package/packages/agentsam-brand/package.json +1 -1
  20. package/packages/agentsam-brand/src/core/v2/compile.js +1 -1
  21. package/packages/agentsam-brand/src/core/v2/schema.js +1 -1
  22. package/packages/agentsam-contracts/package.json +71 -20
  23. package/packages/agentsam-contracts/scripts/build.mjs +18 -0
  24. package/packages/agentsam-contracts/src/errors.ts +19 -10
  25. package/packages/agentsam-contracts/test/contracts.test.mjs +4 -1
  26. package/packages/agentsam-contracts/test/repository.test.mjs +4 -1
  27. package/packages/agentsam-contracts/tsconfig.build.json +10 -0
  28. package/packages/agentsam-errors/package.json +13 -4
  29. package/packages/agentsam-errors/protocol/agentsam.error.v2.schema.json +821 -0
  30. package/packages/agentsam-errors/protocol/error-catalog.json +3512 -0
  31. package/packages/agentsam-errors/src/envelope.js +63 -11
  32. package/packages/agentsam-errors/src/error.js +6 -0
  33. package/packages/agentsam-errors/src/fingerprint.js +9 -1
  34. package/packages/agentsam-errors/src/normalize.js +51 -6
  35. package/packages/agentsam-errors/src/recovery.js +9 -35
  36. package/packages/agentsam-errors/src/transport.js +6 -1
  37. package/packages/agentsam-errors/src/vocabulary.js +515 -126
  38. package/packages/agentsam-errors/test/catalog-v2.test.mjs +106 -0
  39. package/packages/agentsam-errors/types/index.d.ts +157 -0
  40. package/packages/agentsam-goap/package.json +2 -2
  41. package/packages/agentsam-hooks/README.md +206 -0
  42. package/packages/agentsam-hooks/bin/agentsam-hooks.mjs +64 -0
  43. package/packages/agentsam-hooks/bindings/go/command.go +55 -0
  44. package/packages/agentsam-hooks/bindings/go/errors_generated.go +402 -0
  45. package/packages/agentsam-hooks/bindings/go/go.mod +3 -0
  46. package/packages/agentsam-hooks/bindings/go/hooks.go +285 -0
  47. package/packages/agentsam-hooks/bindings/go/hooks_test.go +48 -0
  48. package/packages/agentsam-hooks/bindings/rust/Cargo.toml +11 -0
  49. package/packages/agentsam-hooks/bindings/rust/src/errors_generated.rs +396 -0
  50. package/packages/agentsam-hooks/bindings/rust/src/lib.rs +397 -0
  51. package/packages/agentsam-hooks/package.json +69 -0
  52. package/packages/agentsam-hooks/schema/manifest.json +54 -0
  53. package/packages/agentsam-hooks/scripts/clean-protocol.mjs +17 -0
  54. package/packages/agentsam-hooks/scripts/sync-protocol.mjs +27 -0
  55. package/packages/agentsam-hooks/src/adapters/agents.js +82 -0
  56. package/packages/agentsam-hooks/src/adapters/callback.js +4 -0
  57. package/packages/agentsam-hooks/src/adapters/capability.js +122 -0
  58. package/packages/agentsam-hooks/src/adapters/command.js +104 -0
  59. package/packages/agentsam-hooks/src/adapters/composite.js +31 -0
  60. package/packages/agentsam-hooks/src/adapters/http.js +58 -0
  61. package/packages/agentsam-hooks/src/adapters/lsp.js +116 -0
  62. package/packages/agentsam-hooks/src/adapters/mcp.js +76 -0
  63. package/packages/agentsam-hooks/src/adapters/provider.js +129 -0
  64. package/packages/agentsam-hooks/src/config.js +143 -0
  65. package/packages/agentsam-hooks/src/contracts.js +284 -0
  66. package/packages/agentsam-hooks/src/errors-generated.js +2732 -0
  67. package/packages/agentsam-hooks/src/index.d.ts +193 -0
  68. package/packages/agentsam-hooks/src/index.js +14 -0
  69. package/packages/agentsam-hooks/src/policy.js +34 -0
  70. package/packages/agentsam-hooks/src/runtime.js +312 -0
  71. package/packages/agentsam-hooks/src/store.js +264 -0
  72. package/packages/agentsam-hooks/test/adapters.test.mjs +216 -0
  73. package/packages/agentsam-hooks/test/runtime.test.mjs +258 -0
  74. package/packages/agentsam-knowledge/package.json +3 -2
  75. package/packages/agentsam-nav/package.json +1 -1
  76. package/packages/agentsam-queue-control/package.json +1 -1
  77. package/packages/agentsam-rapid-rust/src/main.rs +1 -0
  78. package/packages/agentsam-repository/package.json +3 -2
  79. package/packages/agentsam-vault/package.json +8 -5
  80. package/packages/agentsam-workbench/package.json +45 -18
  81. package/packages/connectors/cloudflare/package.json +1 -1
  82. package/packages/heuristic-theme/package.json +1 -1
  83. package/packages/identity/package.json +3 -2
  84. package/packages/identity/tests/lane1-packed-consumer.test.mjs +9 -1
  85. package/packages/providers/completeful/package.json +1 -1
  86. package/packages/theme-church-site/README.md +2 -2
  87. package/packages/theme-church-site/package.json +2 -3
  88. package/packages/theme-church-site/src/index.js +1 -1
  89. package/packages/theme-church-site/test/theme.test.mjs +1 -1
  90. package/packages/theme-companions-site/README.md +2 -2
  91. package/packages/theme-companions-site/package.json +2 -3
  92. package/packages/theme-companions-site/src/index.js +1 -1
  93. package/packages/theme-companions-site/test/theme.test.mjs +1 -1
  94. package/packages/theme-floors-site/README.md +2 -2
  95. package/packages/theme-floors-site/package.json +2 -3
  96. package/packages/theme-floors-site/src/index.js +1 -1
  97. package/packages/theme-floors-site/test/theme.test.mjs +1 -1
  98. package/packages/theme-fuelnfree-site/README.md +2 -2
  99. package/packages/theme-fuelnfree-site/package.json +2 -3
  100. package/packages/theme-fuelnfree-site/src/index.js +1 -1
  101. package/packages/theme-fuelnfree-site/test/theme.test.mjs +1 -1
  102. package/packages/theme-handyman-site/README.md +2 -2
  103. package/packages/theme-handyman-site/package.json +2 -3
  104. package/packages/theme-handyman-site/src/index.js +1 -1
  105. package/packages/theme-handyman-site/test/theme.test.mjs +1 -1
  106. package/packages/theme-insurance-site/README.md +2 -2
  107. package/packages/theme-insurance-site/package.json +2 -3
  108. package/packages/theme-insurance-site/src/index.js +1 -1
  109. package/packages/theme-insurance-site/test/theme.test.mjs +1 -1
  110. package/packages/theme-scenes/package.json +1 -1
  111. package/packages/theme-shinshu-site/README.md +2 -2
  112. package/packages/theme-shinshu-site/package.json +2 -3
  113. package/packages/theme-shinshu-site/src/index.js +1 -1
  114. package/packages/theme-shinshu-site/test/theme.test.mjs +1 -1
  115. package/packages/work-graph/package.json +3 -2
  116. package/protocol/apps/agentsam.app.v1.schema.json +1 -1
  117. package/protocol/apps/agentsam.host-install.v1.schema.json +1 -1
  118. package/protocol/assets/asset-record.v1.schema.json +1 -1
  119. package/protocol/brand/brandpack.v1.schema.json +1 -1
  120. package/protocol/capabilities/capability-manifest.schema.json +1 -1
  121. package/protocol/capabilities/knowledge-search-input.schema.json +1 -0
  122. package/protocol/credentials/issue.v1.schema.json +1 -1
  123. package/protocol/database/agentsam.schema-pack.v1.schema.json +1 -1
  124. package/protocol/database/connection.v1.schema.json +1 -1
  125. package/protocol/embeddings/embedding-profile.v1.schema.json +1 -1
  126. package/protocol/errors/README.md +36 -0
  127. package/protocol/errors/agentsam.error.v2.schema.json +821 -0
  128. package/protocol/errors/error-catalog.json +3506 -158
  129. package/protocol/errors/error-envelope.schema.json +195 -52
  130. package/protocol/errors/recovery.v1.schema.json +1 -1
  131. package/protocol/features/agentsam.feature.v1.schema.json +1 -1
  132. package/protocol/hooks/README.md +18 -0
  133. package/protocol/hooks/agentsam.hook.output.v1.schema.json +27 -0
  134. package/protocol/hooks/agentsam.hook.receipt.v1.schema.json +61 -0
  135. package/protocol/hooks/agentsam.hook.v1.schema.json +35 -0
  136. package/protocol/hooks/agentsam.hooks.config.v1.schema.json +78 -0
  137. package/protocol/local-engine/benchmark.v1.schema.json +14 -0
  138. package/protocol/local-engine/engine.v1.schema.json +17 -0
  139. package/protocol/models/model-inventory-v2.schema.json +1 -1
  140. package/protocol/models/provider-model-snapshot.v1.schema.json +22 -0
  141. package/protocol/rpc/v1/errors.proto +1 -0
  142. package/protocol/runtime/agentsam.runtime.v1.schema.json +1 -1
  143. package/protocol/runtime/workspace-fs.v1.schema.json +1 -1
  144. package/protocol/sam/activity.v1.schema.json +1 -1
  145. package/protocol/sam/answer.v1.schema.json +1 -1
  146. package/protocol/sam/calibration.v1.schema.json +1 -1
  147. package/protocol/sam/decision-receipt.v1.schema.json +1 -1
  148. package/protocol/sam/evaluation.v1.schema.json +1 -1
  149. package/protocol/sam/operation.schema.json +1 -1
  150. package/protocol/sam/outcome.v1.schema.json +1 -1
  151. package/protocol/sam/question.v1.schema.json +1 -1
  152. package/protocol/sam/result.schema.json +1 -1
  153. package/protocol/sam/state.v1.schema.json +1 -1
  154. package/protocol/scoring/scorecard.v1.schema.json +1 -1
  155. package/protocol/skills/agentsam.interaction.v1.schema.json +1 -1
  156. package/protocol/skills/agentsam.skill.v1.schema.json +1 -1
  157. package/protocol/theme-refinery/product-metadata.schema.json +1 -1
  158. package/protocol/theme-refinery/reconciliation-receipt.schema.json +1 -1
  159. package/protocol/ui/icon.v1.schema.json +1 -1
  160. package/python/README.md +20 -0
  161. package/python/agentsam_sdk/errors/__init__.py +4 -0
  162. package/python/agentsam_sdk/errors/vocabulary.py +2730 -0
  163. package/python/agentsam_sdk/hooks/__init__.py +32 -0
  164. package/python/agentsam_sdk/hooks/adapters.py +132 -0
  165. package/python/agentsam_sdk/hooks/contracts.py +121 -0
  166. package/python/agentsam_sdk/hooks/runtime.py +253 -0
  167. package/python/tests/test_hooks.py +115 -0
  168. package/src/agent/responses-runner.js +145 -1
  169. package/src/cli/command-catalog.js +2 -0
  170. package/src/cli.js +19 -0
  171. package/src/commands/app.js +10 -0
  172. package/src/commands/cheat-sheet.js +1 -0
  173. package/src/commands/engine.js +10 -0
  174. package/src/commands/hooks.js +260 -0
  175. package/src/commands/machine.js +71 -38
  176. package/src/commands/models.js +1 -1
  177. package/src/commands/package.js +10 -3
  178. package/src/commands/runtime.js +25 -10
  179. package/src/commands/setup.js +30 -6
  180. package/src/commands/shell.js +114 -9
  181. package/src/commands/terminal.js +20 -8
  182. package/src/errors/contract.js +4 -4
  183. package/src/errors/diagnostic.js +10 -10
  184. package/src/errors/index.js +1 -1
  185. package/src/index.js +52 -0
  186. package/src/indexing/ingest/materials.js +237 -30
  187. package/src/knowledge/service/grpc-codec.js +2 -0
  188. package/src/lib/setup/runtime.js +4 -2
  189. package/src/lib/slash-commands.js +1 -0
  190. package/src/local/runtime-store.js +46 -0
  191. package/src/local-engine/assets.js +37 -0
  192. package/src/local-engine/audit.js +133 -0
  193. package/src/local-engine/benchmark.js +26 -0
  194. package/src/local-engine/contracts.js +101 -0
  195. package/src/local-engine/engines.js +142 -0
  196. package/src/local-engine/index.js +18 -0
  197. package/src/local-engine/runtime.js +39 -0
  198. package/src/models/catalog.js +0 -49
  199. package/src/models/inventory-core.js +69 -0
  200. package/src/plugins/index.js +8 -1
  201. package/src/plugins/registry.js +56 -0
  202. package/src/rpc/generated/errors_pb.js +49 -1
  203. package/src/tools/runtime.js +13 -5
@@ -0,0 +1,397 @@
1
+ //! Portable `agentsam.hook.v1` models and ordered callback runtime.
2
+ //! The JSON envelope is shared by callbacks, subprocesses, and HTTP hosts.
3
+
4
+ use serde::{Deserialize, Serialize};
5
+ use serde_json::{Map, Value};
6
+ use std::collections::BTreeMap;
7
+ use std::fmt::{Display, Formatter};
8
+ use std::path::PathBuf;
9
+ use std::sync::Arc;
10
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
+
12
+ mod errors_generated;
13
+ pub use errors_generated::{error_policy, normalize_error_reason, ErrorPolicy, ERROR_REASON_ALIASES, ERROR_REASON_POLICIES, ERROR_SCHEMA_VERSION};
14
+
15
+ pub const PROTOCOL_SCHEMA: &str = "agentsam.hook.v1";
16
+ pub const RECEIPT_SCHEMA: &str = "agentsam.hook.receipt.v1";
17
+ pub const EVENTS: &[&str] = &[
18
+ "session_start", "session_end", "user_prompt_submitted", "user_prompt_transformed",
19
+ "pre_model_use", "post_model_use", "pre_tool_use", "post_tool_use",
20
+ "post_tool_use_failure", "error_occurred", "agent_stop", "subagent_start", "subagent_stop",
21
+ ];
22
+
23
+ fn now_ms() -> u64 {
24
+ SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64
25
+ }
26
+
27
+ fn valid_event(event: &str) -> bool { EVENTS.contains(&event) }
28
+
29
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
30
+ #[serde(deny_unknown_fields)]
31
+ pub struct Invocation {
32
+ #[serde(skip_serializing_if = "Option::is_none")]
33
+ pub session_id: Option<String>,
34
+ #[serde(skip_serializing_if = "Option::is_none")]
35
+ pub run_id: Option<String>,
36
+ #[serde(skip_serializing_if = "Option::is_none")]
37
+ pub turn_id: Option<String>,
38
+ #[serde(skip_serializing_if = "Option::is_none")]
39
+ pub message_id: Option<String>,
40
+ #[serde(skip_serializing_if = "Option::is_none")]
41
+ pub agent_id: Option<String>,
42
+ #[serde(skip_serializing_if = "Option::is_none")]
43
+ pub parent_agent_id: Option<String>,
44
+ #[serde(skip_serializing_if = "Option::is_none")]
45
+ pub source: Option<String>,
46
+ #[serde(default, skip_serializing_if = "Map::is_empty")]
47
+ pub metadata: Map<String, Value>,
48
+ }
49
+
50
+ #[derive(Debug, Clone, Serialize, Deserialize)]
51
+ #[serde(deny_unknown_fields)]
52
+ pub struct Envelope {
53
+ pub schema: String,
54
+ pub hook: String,
55
+ pub timestamp: u64,
56
+ pub cwd: String,
57
+ #[serde(default)]
58
+ pub invocation: Invocation,
59
+ #[serde(default)]
60
+ pub input: Map<String, Value>,
61
+ }
62
+
63
+ impl Envelope {
64
+ pub fn new(hook: impl Into<String>, cwd: impl Into<String>, invocation: Invocation, input: Map<String, Value>) -> Result<Self, HookError> {
65
+ let hook = hook.into();
66
+ if !valid_event(&hook) { return Err(HookError::new(format!("unsupported_hook_event:{hook}"))); }
67
+ Ok(Self { schema: PROTOCOL_SCHEMA.into(), hook, timestamp: now_ms(), cwd: cwd.into(), invocation, input })
68
+ }
69
+ }
70
+
71
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
72
+ #[serde(deny_unknown_fields)]
73
+ pub struct Output {
74
+ #[serde(skip_serializing_if = "Option::is_none")]
75
+ pub permission_decision: Option<String>,
76
+ #[serde(skip_serializing_if = "Option::is_none")]
77
+ pub permission_decision_reason: Option<String>,
78
+ #[serde(skip_serializing_if = "Option::is_none")]
79
+ pub modified_args: Option<Map<String, Value>>,
80
+ #[serde(skip_serializing_if = "Option::is_none")]
81
+ pub modified_request: Option<Map<String, Value>>,
82
+ #[serde(skip_serializing_if = "Option::is_none")]
83
+ pub modified_result: Option<Value>,
84
+ #[serde(skip_serializing_if = "Option::is_none")]
85
+ pub modified_prompt: Option<String>,
86
+ #[serde(skip_serializing_if = "Option::is_none")]
87
+ pub modified_transformed_prompt: Option<String>,
88
+ #[serde(skip_serializing_if = "Option::is_none")]
89
+ pub modified_config: Option<Map<String, Value>>,
90
+ #[serde(skip_serializing_if = "Option::is_none")]
91
+ pub additional_context: Option<String>,
92
+ #[serde(skip_serializing_if = "Option::is_none")]
93
+ pub suppress_output: Option<bool>,
94
+ #[serde(skip_serializing_if = "Option::is_none")]
95
+ pub error_handling: Option<String>,
96
+ #[serde(skip_serializing_if = "Option::is_none")]
97
+ pub retry_count: Option<u8>,
98
+ #[serde(skip_serializing_if = "Option::is_none")]
99
+ pub user_notification: Option<String>,
100
+ #[serde(skip_serializing_if = "Option::is_none")]
101
+ pub decision: Option<String>,
102
+ #[serde(skip_serializing_if = "Option::is_none")]
103
+ pub reason: Option<String>,
104
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
105
+ pub cleanup_actions: Vec<String>,
106
+ #[serde(skip_serializing_if = "Option::is_none")]
107
+ pub session_summary: Option<String>,
108
+ #[serde(default, skip_serializing_if = "Map::is_empty")]
109
+ pub metadata: Map<String, Value>,
110
+ }
111
+
112
+ impl Output {
113
+ pub fn validate(&self, event: &str) -> Result<(), HookError> {
114
+ if let Some(decision) = &self.permission_decision {
115
+ if !["allow", "deny", "ask"].contains(&decision.as_str()) { return Err(HookError::new(format!("invalid_permission_decision:{decision}"))); }
116
+ if !["pre_tool_use", "pre_model_use"].contains(&event) { return Err(HookError::new(format!("permission_decision_not_supported:{event}"))); }
117
+ }
118
+ if let Some(handling) = &self.error_handling {
119
+ if !["retry", "skip", "abort"].contains(&handling.as_str()) { return Err(HookError::new(format!("invalid_error_handling:{handling}"))); }
120
+ if event != "error_occurred" { return Err(HookError::new(format!("error_handling_not_supported:{event}"))); }
121
+ }
122
+ if self.retry_count.unwrap_or(0) > 10 {
123
+ return Err(HookError::new("retry_count_must_be_between_0_and_10"));
124
+ }
125
+ if let Some(decision) = &self.decision {
126
+ if event != "agent_stop" || !["allow", "block"].contains(&decision.as_str()) { return Err(HookError::new(format!("invalid_stop_decision:{decision}"))); }
127
+ }
128
+ if self.modified_config.is_some() && event != "session_start" {
129
+ return Err(HookError::new(format!("modified_config_not_supported:{event}")));
130
+ }
131
+ Ok(())
132
+ }
133
+ }
134
+
135
+ #[derive(Debug, Clone)]
136
+ pub struct HookError { pub message: String }
137
+ impl HookError { pub fn new(message: impl Into<String>) -> Self { Self { message: message.into() } } }
138
+ impl Display for HookError { fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { formatter.write_str(&self.message) } }
139
+ impl std::error::Error for HookError {}
140
+
141
+ pub type Handler = Arc<dyn Fn(&Envelope) -> Result<Option<Output>, HookError> + Send + Sync>;
142
+
143
+ #[derive(Clone)]
144
+ pub struct Definition {
145
+ pub id: String,
146
+ pub priority: i32,
147
+ pub timeout: Duration,
148
+ pub failure_mode: Option<String>,
149
+ pub enabled: bool,
150
+ pub handler: Handler,
151
+ }
152
+
153
+ impl Definition {
154
+ pub fn new(id: impl Into<String>, handler: Handler) -> Self {
155
+ Self { id: id.into(), priority: 100, timeout: Duration::from_secs(10), failure_mode: None, enabled: true, handler }
156
+ }
157
+ }
158
+
159
+ #[derive(Debug, Clone, Serialize, Deserialize)]
160
+ pub struct Receipt {
161
+ pub schema: String,
162
+ pub hook_id: String,
163
+ pub hook: String,
164
+ pub status: String,
165
+ pub started_at: u64,
166
+ pub completed_at: u64,
167
+ pub duration_ms: u64,
168
+ pub input_keys: Vec<String>,
169
+ pub output_keys: Vec<String>,
170
+ #[serde(skip_serializing_if = "Option::is_none")]
171
+ pub error: Option<Value>,
172
+ #[serde(skip_serializing_if = "Option::is_none")]
173
+ pub native_evidence: Option<Value>,
174
+ }
175
+
176
+ #[derive(Debug, Clone, Serialize, Deserialize)]
177
+ pub struct DispatchResult {
178
+ pub schema: String,
179
+ pub hook: String,
180
+ pub input: Map<String, Value>,
181
+ pub output: Output,
182
+ pub receipts: Vec<Receipt>,
183
+ pub errors: Vec<Value>,
184
+ }
185
+
186
+ #[derive(Default)]
187
+ pub struct Runtime { hooks: BTreeMap<String, Vec<Definition>> }
188
+
189
+ impl Runtime {
190
+ pub fn new() -> Self { Self::default() }
191
+
192
+ pub fn register(&mut self, event: &str, mut definition: Definition) -> Result<(), HookError> {
193
+ if !valid_event(event) { return Err(HookError::new(format!("unsupported_hook_event:{event}"))); }
194
+ if definition.id.trim().is_empty() { return Err(HookError::new("hook_id_required")); }
195
+ if definition.failure_mode.is_none() {
196
+ definition.failure_mode = Some(if ["pre_tool_use", "pre_model_use"].contains(&event) { "closed" } else { "open" }.into());
197
+ }
198
+ let rows = self.hooks.entry(event.into()).or_default();
199
+ if rows.iter().any(|row| row.id == definition.id) { return Err(HookError::new(format!("duplicate_hook_id:{}", definition.id))); }
200
+ rows.push(definition); rows.sort_by(|a, b| a.priority.cmp(&b.priority).then(a.id.cmp(&b.id)));
201
+ Ok(())
202
+ }
203
+
204
+ pub fn dispatch(&self, event: &str, cwd: &str, invocation: Invocation, input: Map<String, Value>) -> Result<DispatchResult, HookError> {
205
+ if !valid_event(event) { return Err(HookError::new(format!("unsupported_hook_event:{event}"))); }
206
+ let mut working = input; let mut combined = Output::default(); let mut contexts = Vec::new(); let mut receipts = Vec::new(); let mut errors = Vec::new();
207
+ for definition in self.hooks.get(event).into_iter().flatten().filter(|row| row.enabled) {
208
+ let envelope = Envelope::new(event, cwd, invocation.clone(), working.clone())?; let started = now_ms();
209
+ match (definition.handler)(&envelope).and_then(|output| { if let Some(value) = &output { value.validate(event)?; } Ok(output) }) {
210
+ Ok(output) => {
211
+ let output = output.unwrap_or_default(); apply(event, &mut working, &output); merge(&mut combined, &output, &mut contexts);
212
+ let completed = now_ms(); receipts.push(receipt(definition, event, started, completed, "completed", &envelope.input, &output, None));
213
+ if (["pre_tool_use", "pre_model_use"].contains(&event) && matches!(output.permission_decision.as_deref(), Some("deny" | "ask"))) || (event == "agent_stop" && output.decision.as_deref() == Some("block")) { break; }
214
+ }
215
+ Err(error) => {
216
+ let completed = now_ms();
217
+ let failed_receipt = receipt(definition, event, started, completed, "failed", &envelope.input, &Output::default(), Some(&error));
218
+ let mut failure = failed_receipt.error.clone().unwrap_or_else(|| Value::Object(Map::new()));
219
+ if let Some(object) = failure.as_object_mut() {
220
+ object.insert("hook_id".into(), definition.id.clone().into());
221
+ object.insert("hook".into(), event.into());
222
+ object.insert("native_evidence".into(), failed_receipt.native_evidence.clone().unwrap_or(Value::Null));
223
+ }
224
+ receipts.push(failed_receipt); errors.push(failure);
225
+ match definition.failure_mode.as_deref().unwrap_or("open") {
226
+ "error" => return Err(HookError::new(format!("hook_execution_failed:{}:{error}", definition.id))),
227
+ "closed" => { combined.permission_decision = Some("deny".into()); combined.permission_decision_reason = Some(format!("Hook '{}' failed closed: {}", definition.id, safe_hook_error(&error))); break; }
228
+ _ => {}
229
+ }
230
+ }
231
+ }
232
+ }
233
+ if !contexts.is_empty() { combined.additional_context = Some(contexts.join("\n\n")); }
234
+ Ok(DispatchResult { schema: "agentsam.hook.dispatch.v1".into(), hook: event.into(), input: working, output: combined, receipts, errors })
235
+ }
236
+ }
237
+
238
+ fn apply(event: &str, input: &mut Map<String, Value>, output: &Output) {
239
+ if let Some(value) = &output.modified_args { input.insert("tool_args".into(), Value::Object(value.clone())); }
240
+ if let Some(value) = &output.modified_request { input.insert("request".into(), Value::Object(value.clone())); }
241
+ if let Some(value) = &output.modified_result { input.insert(if event == "post_tool_use" { "tool_result" } else { "model_result" }.into(), value.clone()); }
242
+ if let Some(value) = &output.modified_prompt { input.insert("prompt".into(), value.clone().into()); }
243
+ if let Some(value) = &output.modified_transformed_prompt { input.insert("transformed_prompt".into(), value.clone().into()); }
244
+ if let Some(value) = &output.modified_config {
245
+ let mut config = input.get("config").and_then(Value::as_object).cloned().unwrap_or_default();
246
+ config.extend(value.clone()); input.insert("config".into(), Value::Object(config));
247
+ }
248
+ }
249
+
250
+ fn merge(target: &mut Output, update: &Output, contexts: &mut Vec<String>) {
251
+ if update.permission_decision.is_some() { target.permission_decision = update.permission_decision.clone(); target.permission_decision_reason = update.permission_decision_reason.clone(); }
252
+ if update.modified_args.is_some() { target.modified_args = update.modified_args.clone(); } if update.modified_request.is_some() { target.modified_request = update.modified_request.clone(); }
253
+ if update.modified_result.is_some() { target.modified_result = update.modified_result.clone(); } if update.modified_prompt.is_some() { target.modified_prompt = update.modified_prompt.clone(); }
254
+ if update.modified_transformed_prompt.is_some() { target.modified_transformed_prompt = update.modified_transformed_prompt.clone(); }
255
+ if let Some(value) = &update.modified_config {
256
+ target.modified_config.get_or_insert_with(Map::new).extend(value.clone());
257
+ }
258
+ if let Some(value) = &update.additional_context { contexts.push(value.clone()); }
259
+ target.suppress_output = Some(target.suppress_output.unwrap_or(false) || update.suppress_output.unwrap_or(false));
260
+ if update.error_handling.is_some() { target.error_handling = update.error_handling.clone(); } target.retry_count = Some(target.retry_count.unwrap_or(0).max(update.retry_count.unwrap_or(0)));
261
+ if update.user_notification.is_some() { target.user_notification = update.user_notification.clone(); } if update.decision.is_some() { target.decision = update.decision.clone(); target.reason = update.reason.clone(); }
262
+ target.cleanup_actions.extend(update.cleanup_actions.clone()); if update.session_summary.is_some() { target.session_summary = update.session_summary.clone(); }
263
+ }
264
+
265
+ fn safe_hook_error(error: &HookError) -> String {
266
+ let message = error.to_string();
267
+ let lower = message.to_ascii_lowercase();
268
+ if ["authorization:", "authorization=", "bearer ", "api_key", "api-key", "password", "secret", "token"]
269
+ .iter().any(|marker| lower.contains(marker))
270
+ {
271
+ return "[REDACTED hook error; sensitive native evidence removed]".into();
272
+ }
273
+ message.chars().take(512).collect()
274
+ }
275
+
276
+ fn hook_fingerprint(values: &[&str]) -> String {
277
+ let text = values.join("\u{1f}");
278
+ let mut hash: u64 = 0xcbf29ce484222325;
279
+ for byte in text.as_bytes() { hash ^= u64::from(*byte); hash = hash.wrapping_mul(0x100000001b3); }
280
+ format!("err_{hash:016x}")
281
+ }
282
+
283
+ fn canonical_hook_failure(definition: &Definition, event: &str, error: &HookError) -> (Value, Value) {
284
+ let mut reason = normalize_error_reason(&[&error.message]);
285
+ let hook_specific;
286
+ if reason.starts_with("mcp_") || reason.starts_with("lsp_") {
287
+ hook_specific = format!("hook_{reason}");
288
+ if let Some(policy) = error_policy(&hook_specific) { reason = policy.reason; }
289
+ }
290
+ let policy = error_policy(reason).expect("generated hook error policy");
291
+ let message = safe_hook_error(error);
292
+ let is_post = event == "post_tool_use" || event == "post_model_use";
293
+ let behavior = match definition.failure_mode.as_deref() {
294
+ Some("closed") => "fail_closed",
295
+ Some("open") => "fail_open",
296
+ Some("error") if is_post => "no_replay",
297
+ _ => "abort",
298
+ };
299
+ let side_effect = if is_post { "confirmed_applied" } else { "not_started" };
300
+ let adapter = ["http", "mcp", "lsp", "command"].iter().find(|name| reason.starts_with(&format!("hook_{name}_"))).copied();
301
+ let protocol = adapter.map(|value| if value == "command" { PROTOCOL_SCHEMA } else { value });
302
+ let transport = adapter.and_then(|value| if value == "command" { Some("process") } else { None });
303
+ let feature = format!("hooks.{event}");
304
+ let retryable = policy.retryable && !is_post && behavior != "fail_closed";
305
+ let fingerprint = hook_fingerprint(&[
306
+ policy.code, reason, policy.domain, policy.failure_class, policy.default_stage, &feature,
307
+ behavior, side_effect, adapter.unwrap_or(""), protocol.unwrap_or(""), transport.unwrap_or(""), &definition.id,
308
+ ]);
309
+ let canonical = serde_json::json!({
310
+ "error_code": policy.code, "reason": reason, "domain": policy.domain,
311
+ "failure_class": policy.failure_class, "stage": policy.default_stage, "feature": feature,
312
+ "failure_behavior": behavior, "retryable": retryable, "side_effect_state": side_effect,
313
+ "adapter": adapter, "protocol": protocol, "transport": transport,
314
+ "fingerprint": fingerprint, "message": message.clone(),
315
+ });
316
+ let native = serde_json::json!({"code": null, "exception_type": "HookError", "message": message});
317
+ (canonical, native)
318
+ }
319
+
320
+ fn receipt(definition: &Definition, event: &str, started: u64, completed: u64, status: &str, input: &Map<String, Value>, output: &Output, error: Option<&HookError>) -> Receipt {
321
+ let mut input_keys: Vec<_> = input.keys().cloned().collect(); input_keys.sort();
322
+ let object = serde_json::to_value(output).ok().and_then(|value| value.as_object().cloned()).unwrap_or_default(); let mut output_keys: Vec<_> = object.keys().cloned().collect(); output_keys.sort();
323
+ let (error, native_evidence) = error.map(|value| canonical_hook_failure(definition, event, value)).map_or((None, None), |(canonical, native)| (Some(canonical), Some(native)));
324
+ Receipt { schema: RECEIPT_SCHEMA.into(), hook_id: definition.id.clone(), hook: event.into(), status: status.into(), started_at: started, completed_at: completed, duration_ms: completed.saturating_sub(started), input_keys, output_keys, error, native_evidence }
325
+ }
326
+
327
+ #[derive(Debug, Clone)]
328
+ pub struct CommandOptions { pub command: String, pub args: Vec<String>, pub cwd: Option<PathBuf>, pub env: BTreeMap<String, String>, pub inherit_environment: bool, pub max_output_bytes: usize }
329
+
330
+ impl CommandOptions {
331
+ pub fn handler(self) -> Handler {
332
+ Arc::new(move |envelope| {
333
+ let mut command = std::process::Command::new(&self.command); command.args(&self.args).current_dir(self.cwd.as_deref().unwrap_or_else(|| std::path::Path::new(&envelope.cwd))).stdin(std::process::Stdio::piped()).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped());
334
+ if !self.inherit_environment { command.env_clear(); for key in ["PATH", "HOME", "USERPROFILE", "TMPDIR", "TMP", "TEMP", "SYSTEMROOT"] { if let Some(value) = std::env::var_os(key) { command.env(key, value); } } }
335
+ command.envs(&self.env); let mut child = command.spawn().map_err(|error| HookError::new(format!("hook_command_spawn_failed:{error}")))?;
336
+ { use std::io::Write; let input = child.stdin.as_mut().ok_or_else(|| HookError::new("hook_command_stdin_unavailable"))?; serde_json::to_writer(&mut *input, envelope).map_err(|error| HookError::new(error.to_string()))?; input.write_all(b"\n").map_err(|error| HookError::new(error.to_string()))?; }
337
+ let result = child.wait_with_output().map_err(|error| HookError::new(format!("hook_command_failed:{error}")))?; let limit = if self.max_output_bytes == 0 { 1_048_576 } else { self.max_output_bytes };
338
+ if result.stdout.len() + result.stderr.len() > limit { return Err(HookError::new(format!("hook_command_output_limit_exceeded:{limit}"))); }
339
+ if !result.status.success() { return Err(HookError::new(format!("hook_command_failed:{}", String::from_utf8_lossy(&result.stderr)))); }
340
+ let stdout = String::from_utf8_lossy(&result.stdout); let line = stdout.lines().rev().find(|line| !line.trim().is_empty()); match line { None => Ok(None), Some(value) => serde_json::from_str(value).map(Some).map_err(|error| HookError::new(format!("hook_command_invalid_json:{error}"))) }
341
+ })
342
+ }
343
+ }
344
+
345
+ #[cfg(test)]
346
+ mod tests {
347
+ use super::*;
348
+ use serde_json::json;
349
+
350
+ #[test]
351
+ fn composes_portable_pre_tool_hook() {
352
+ let mut runtime = Runtime::new();
353
+ let handler: Handler = Arc::new(|envelope| {
354
+ let mut args = envelope.input.get("tool_args").and_then(Value::as_object).cloned().unwrap_or_default();
355
+ args.insert("bounded".into(), true.into());
356
+ Ok(Some(Output {
357
+ permission_decision: Some("allow".into()),
358
+ modified_args: Some(args),
359
+ additional_context: Some("Rust policy".into()),
360
+ ..Output::default()
361
+ }))
362
+ });
363
+ runtime.register("pre_tool_use", Definition::new("policy", handler)).unwrap();
364
+ let input = json!({"tool_name":"read","tool_args":{"value":1}}).as_object().unwrap().clone();
365
+ let result = runtime.dispatch("pre_tool_use", ".", Invocation::default(), input).unwrap();
366
+ assert_eq!(result.output.permission_decision.as_deref(), Some("allow"));
367
+ assert_eq!(result.output.additional_context.as_deref(), Some("Rust policy"));
368
+ assert_eq!(result.input["tool_args"]["bounded"], true);
369
+ assert_eq!(result.receipts[0].schema, RECEIPT_SCHEMA);
370
+ }
371
+
372
+ #[test]
373
+ fn generated_error_aliases_share_canonical_policy() {
374
+ let reason = normalize_error_reason(&["AGENTSAM_HOOK_HTTP_FAILED"]);
375
+ assert_eq!(reason, "hook_http_request_failed");
376
+ let policy = error_policy(reason).unwrap();
377
+ assert_eq!(policy.code, "UNAVAILABLE");
378
+ assert_eq!(policy.domain, "hook");
379
+ assert!(policy.retryable);
380
+ }
381
+
382
+ #[test]
383
+ fn failed_post_hook_records_no_replay_and_applied_side_effect() {
384
+ let mut runtime = Runtime::new();
385
+ let handler: Handler = Arc::new(|_| Err(HookError::new("hook_mcp_timeout:server")));
386
+ let mut definition = Definition::new("observer", handler);
387
+ definition.failure_mode = Some("open".into());
388
+ runtime.register("post_tool_use", definition).unwrap();
389
+ let result = runtime.dispatch("post_tool_use", ".", Invocation::default(), Map::new()).unwrap();
390
+ let error = result.receipts[0].error.as_ref().unwrap();
391
+ assert_eq!(error["error_code"], "DEADLINE_EXCEEDED");
392
+ assert_eq!(error["reason"], "hook_mcp_timeout");
393
+ assert_eq!(error["failure_behavior"], "fail_open");
394
+ assert_eq!(error["side_effect_state"], "confirmed_applied");
395
+ assert_eq!(error["retryable"], false);
396
+ }
397
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@inneranimalmedia/agentsam-hooks",
3
+ "version": "2.6.10",
4
+ "description": "Provider-neutral, language-agnostic AgentSam lifecycle hooks with callback, command, HTTP, model, tool, MCP, LSP, and sub-agent adapters.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js"
12
+ },
13
+ "./contracts": "./src/contracts.js",
14
+ "./runtime": "./src/runtime.js",
15
+ "./config": "./src/config.js",
16
+ "./store": "./src/store.js",
17
+ "./adapters/callback": "./src/adapters/callback.js",
18
+ "./adapters/command": "./src/adapters/command.js",
19
+ "./adapters/http": "./src/adapters/http.js",
20
+ "./adapters/provider": "./src/adapters/provider.js",
21
+ "./adapters/capability": "./src/adapters/capability.js",
22
+ "./adapters/composite": "./src/adapters/composite.js",
23
+ "./adapters/mcp": "./src/adapters/mcp.js",
24
+ "./adapters/lsp": "./src/adapters/lsp.js",
25
+ "./adapters/agents": "./src/adapters/agents.js",
26
+ "./protocol/input": "./protocol/agentsam.hook.v1.schema.json",
27
+ "./protocol/output": "./protocol/agentsam.hook.output.v1.schema.json",
28
+ "./protocol/config": "./protocol/agentsam.hooks.config.v1.schema.json",
29
+ "./schema/manifest": "./schema/manifest.json",
30
+ "./schema/sqlite": "./schema/migrations/sqlite/001_hooks_core.sql",
31
+ "./package.json": "./package.json",
32
+ "./protocol/receipt": "./protocol/agentsam.hook.receipt.v1.schema.json"
33
+ },
34
+ "bin": {
35
+ "agentsam-hooks": "./bin/agentsam-hooks.mjs"
36
+ },
37
+ "files": [
38
+ "bin",
39
+ "bindings",
40
+ "protocol",
41
+ "scripts",
42
+ "schema",
43
+ "src",
44
+ "README.md"
45
+ ],
46
+ "scripts": {
47
+ "sync:protocol": "node scripts/sync-protocol.mjs",
48
+ "clean:protocol": "node scripts/clean-protocol.mjs",
49
+ "build": "npm run sync:protocol",
50
+ "pretest": "npm run sync:protocol",
51
+ "test": "node --test test/*.test.mjs",
52
+ "posttest": "npm run clean:protocol",
53
+ "typecheck": "tsc --noEmit --allowJs false --skipLibCheck src/index.d.ts",
54
+ "prepack": "npm run sync:protocol",
55
+ "postpack": "npm run clean:protocol"
56
+ },
57
+ "engines": {
58
+ "node": ">=22 <25"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "license": "MIT",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/SamPrimeaux/agentsam-sdk.git",
67
+ "directory": "packages/agentsam-hooks"
68
+ }
69
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "https://schemas.inneranimalmedia.com/agentsam/database/agentsam.schema-pack.v1.schema.json",
3
+ "schema": "agentsam.schema-pack.v1",
4
+ "id": "agentsam.hooks",
5
+ "version": "1.0.0",
6
+ "pack_family": "agentsam.hooks",
7
+ "components": [
8
+ "hooks.core"
9
+ ],
10
+ "component_dependencies": {
11
+ "hooks.core": []
12
+ },
13
+ "adapters": [
14
+ "sqlite",
15
+ "portable-d1",
16
+ "injected-sql"
17
+ ],
18
+ "sql": {
19
+ "portable_dir": "migrations/sqlite",
20
+ "files": {
21
+ "hooks.core": [
22
+ "001_hooks_core.sql"
23
+ ]
24
+ },
25
+ "apply_order_fresh": [
26
+ "001_hooks_core.sql"
27
+ ],
28
+ "apply_order_upgrade_v1_to_v2": [],
29
+ "optional_after_core": []
30
+ },
31
+ "tables": {
32
+ "hooks.core": [
33
+ "agentsam_hook",
34
+ "agentsam_hook_execution"
35
+ ]
36
+ },
37
+ "table_patterns": [
38
+ "agentsam_hook*"
39
+ ],
40
+ "seed_policy": "none",
41
+ "compatibility": {
42
+ "legacy-hosted": {
43
+ "owns_portable_schema": false,
44
+ "notes": "Hosted tables with tenant_id/workspace_id/user_id require an explicit host-owned rebuild; the portable pack never guesses account ownership."
45
+ }
46
+ },
47
+ "recovery": {
48
+ "authority": "execution receipts",
49
+ "storage": "value-free input/output key lists plus redacted errors",
50
+ "retention": "host policy"
51
+ },
52
+ "owner": "@inneranimalmedia/agentsam-hooks",
53
+ "notes": "Portable hook definitions use opaque owner_id plus scope_type/scope_ref instead of tenant/workspace columns. Code and config hooks share the event and receipt contracts but do not need to be materialized as definition rows. Secrets remain runtime-owned references."
54
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
6
+ const canonical = path.resolve(packageRoot, '../..', 'protocol', 'hooks');
7
+ if (!fs.existsSync(canonical)) process.exit(0);
8
+ const target = path.join(packageRoot, 'protocol');
9
+ for (const filename of [
10
+ 'agentsam.hook.v1.schema.json',
11
+ 'agentsam.hook.output.v1.schema.json',
12
+ 'agentsam.hook.receipt.v1.schema.json',
13
+ 'agentsam.hooks.config.v1.schema.json',
14
+ ]) {
15
+ fs.rmSync(path.join(target, filename), { force: true });
16
+ }
17
+ fs.rmSync(path.join(packageRoot, 'schema', 'migrations', 'sqlite', '001_hooks_core.sql'), { force: true });
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
6
+ const repositoryRoot = path.resolve(packageRoot, '../..');
7
+ const source = path.join(repositoryRoot, 'protocol', 'hooks');
8
+ const target = path.join(packageRoot, 'protocol');
9
+ const files = [
10
+ 'agentsam.hook.v1.schema.json',
11
+ 'agentsam.hook.output.v1.schema.json',
12
+ 'agentsam.hook.receipt.v1.schema.json',
13
+ 'agentsam.hooks.config.v1.schema.json',
14
+ ];
15
+ fs.mkdirSync(target, { recursive: true });
16
+ for (const filename of files) {
17
+ const sourceFile = path.join(source, filename);
18
+ const targetFile = path.join(target, filename);
19
+ if (fs.existsSync(sourceFile)) fs.copyFileSync(sourceFile, targetFile);
20
+ else if (!fs.existsSync(targetFile)) throw new Error(`Canonical hook protocol is unavailable: ${sourceFile}`);
21
+ }
22
+
23
+ const migrationSource = path.join(repositoryRoot, 'migrations', 'runtime', '0005_agentsam_hooks.sql');
24
+ const migrationTarget = path.join(packageRoot, 'schema', 'migrations', 'sqlite', '001_hooks_core.sql');
25
+ fs.mkdirSync(path.dirname(migrationTarget), { recursive: true });
26
+ if (fs.existsSync(migrationSource)) fs.copyFileSync(migrationSource, migrationTarget);
27
+ else if (!fs.existsSync(migrationTarget)) throw new Error(`Canonical hook migration is unavailable: ${migrationSource}`);
@@ -0,0 +1,82 @@
1
+ import { ensureHookRuntime } from '../runtime.js';
2
+
3
+ function normalizeAgent(value) {
4
+ const source = typeof value === 'string' ? { id: value } : value;
5
+ const id = String(source?.id || source?.name || '').trim();
6
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(id)) throw new Error(`invalid_agent_id:${id || '<missing>'}`);
7
+ return Object.freeze({
8
+ id,
9
+ description: String(source.description || `Delegate bounded work to the ${id} agent.`),
10
+ metadata: source.metadata && typeof source.metadata === 'object' ? structuredClone(source.metadata) : {},
11
+ });
12
+ }
13
+
14
+ export function createAgentCapabilityAdapter(options = {}) {
15
+ if (typeof options.runAgent !== 'function') throw new TypeError('run_agent_adapter_required');
16
+ const agents = (options.agents || []).map(normalizeAgent);
17
+ const hooks = ensureHookRuntime(options.hooks || options.hookRuntime);
18
+ const byCapability = new Map(agents.map((agent) => [`agent.delegate.${agent.id}`, agent]));
19
+ const parentInvocation = options.invocation || {};
20
+
21
+ return Object.freeze({
22
+ toolDescriptors() {
23
+ return [...byCapability].map(([name, agent]) => Object.freeze({
24
+ name,
25
+ description: agent.description,
26
+ category: 'agents',
27
+ risk: 'write',
28
+ strict: false,
29
+ input_schema: {
30
+ type: 'object',
31
+ properties: {
32
+ task: { type: 'string', minLength: 1, description: 'Bounded objective delegated to the sub-agent.' },
33
+ context: { type: 'object', description: 'Portable context references or structured evidence for the task.' },
34
+ },
35
+ required: ['task'],
36
+ additionalProperties: false,
37
+ },
38
+ }));
39
+ },
40
+ canInvoke(id) { return byCapability.has(String(id)); },
41
+ async invoke(id, input = {}, context = {}) {
42
+ const agent = byCapability.get(String(id));
43
+ if (!agent) throw new Error(`agent_capability_unavailable:${id}`);
44
+ const task = String(input.task || '').trim();
45
+ if (!task) throw new TypeError('subagent_task_required');
46
+ const agentInvocation = {
47
+ ...parentInvocation,
48
+ parent_agent_id: parentInvocation.agent_id || null,
49
+ agent_id: agent.id,
50
+ };
51
+ await hooks.dispatch('subagent_start', {
52
+ agent_id: agent.id,
53
+ task,
54
+ context: input.context || {},
55
+ }, agentInvocation, { cwd: context.cwd || options.cwd });
56
+ try {
57
+ const result = await options.runAgent({
58
+ agent,
59
+ task,
60
+ context: input.context || {},
61
+ parent: context,
62
+ invocation: agentInvocation,
63
+ });
64
+ await hooks.dispatch('subagent_stop', {
65
+ agent_id: agent.id,
66
+ task,
67
+ status: 'completed',
68
+ result,
69
+ }, agentInvocation, { cwd: context.cwd || options.cwd });
70
+ return result;
71
+ } catch (error) {
72
+ await hooks.dispatch('subagent_stop', {
73
+ agent_id: agent.id,
74
+ task,
75
+ status: 'failed',
76
+ error: { code: error?.code || null, message: String(error?.message || error) },
77
+ }, agentInvocation, { cwd: context.cwd || options.cwd });
78
+ throw error;
79
+ }
80
+ },
81
+ });
82
+ }
@@ -0,0 +1,4 @@
1
+ export function createCallbackHookAdapter(handler) {
2
+ if (typeof handler !== 'function') throw new TypeError('callback_hook_handler_required');
3
+ return async (envelope) => handler(envelope.input, envelope.invocation, envelope);
4
+ }