@deepstrike/core 0.1.4 → 0.1.7
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.
- package/README.md +16 -0
- package/index.d.ts +10 -2
- package/index.js +84 -303
- package/package.json +15 -6
- package/Cargo.toml +0 -22
- package/build.rs +0 -3
- package/npm/darwin-arm64/README.md +0 -22
- package/npm/darwin-arm64/package.json +0 -16
- package/npm/darwin-x64/README.md +0 -22
- package/npm/darwin-x64/package.json +0 -16
- package/npm/linux-arm64-gnu/README.md +0 -22
- package/npm/linux-arm64-gnu/package.json +0 -19
- package/npm/linux-arm64-musl/README.md +0 -22
- package/npm/linux-arm64-musl/package.json +0 -19
- package/npm/linux-x64-gnu/README.md +0 -22
- package/npm/linux-x64-gnu/package.json +0 -19
- package/npm/linux-x64-musl/README.md +0 -22
- package/npm/linux-x64-musl/package.json +0 -19
- package/npm/win32-x64-msvc/README.md +0 -22
- package/npm/win32-x64-msvc/package.json +0 -16
- package/src/lib.rs +0 -1226
package/src/lib.rs
DELETED
|
@@ -1,1226 +0,0 @@
|
|
|
1
|
-
//! # DeepStrike Node.js Bindings
|
|
2
|
-
//!
|
|
3
|
-
//! napi-rs bindings exposing the Rust kernel to Node.js.
|
|
4
|
-
//! Build with: `napi build --release --platform`
|
|
5
|
-
//!
|
|
6
|
-
//! ## High-level API
|
|
7
|
-
//!
|
|
8
|
-
//! ```typescript
|
|
9
|
-
//! import {
|
|
10
|
-
//! ContextEngine, LoopStateMachine, RuntimeTask, LoopPolicy,
|
|
11
|
-
//! Message, ToolCall, ToolResult, ToolSchema,
|
|
12
|
-
//! SkillMetadata,
|
|
13
|
-
//! } from '@deepstrike/core'
|
|
14
|
-
//!
|
|
15
|
-
//! const sm = new LoopStateMachine({ maxTokens: 128_000 })
|
|
16
|
-
//! // Register skills once; the kernel auto-injects the `skill` meta-tool.
|
|
17
|
-
//! sm.setAvailableSkills([
|
|
18
|
-
//! { name: 'debug', description: 'Debug helper', estimatedTokens: 0 },
|
|
19
|
-
//! ])
|
|
20
|
-
//!
|
|
21
|
-
//! let action = sm.start({ goal: 'Fix the bug' })
|
|
22
|
-
//! while (!sm.isTerminal()) {
|
|
23
|
-
//! if (action.kind === 'call_llm') {
|
|
24
|
-
//! // tools list already includes the `skill` meta-tool
|
|
25
|
-
//! const msg = await callLlm(action.messages, action.tools)
|
|
26
|
-
//! action = sm.feedLlmResponse(msg)
|
|
27
|
-
//! } else if (action.kind === 'execute_tools') {
|
|
28
|
-
//! // SDK intercepts calls where name === 'skill' and reads the file
|
|
29
|
-
//! const results = await execTools(action.calls)
|
|
30
|
-
//! action = sm.feedToolResults(results)
|
|
31
|
-
//! } else if (action.kind === 'done') {
|
|
32
|
-
//! break
|
|
33
|
-
//! }
|
|
34
|
-
//! }
|
|
35
|
-
//! ```
|
|
36
|
-
|
|
37
|
-
#![deny(clippy::all)]
|
|
38
|
-
|
|
39
|
-
use napi::bindgen_prelude::*;
|
|
40
|
-
use napi_derive::napi;
|
|
41
|
-
|
|
42
|
-
use compact_str::CompactString;
|
|
43
|
-
|
|
44
|
-
use deepstrike_core::context::manager::ContextManager;
|
|
45
|
-
use deepstrike_core::context::pressure::PressureAction;
|
|
46
|
-
use deepstrike_core::governance::permission::{PermissionAction, PermissionRule};
|
|
47
|
-
use deepstrike_core::governance::pipeline::GovernancePipeline as RustGovernancePipeline;
|
|
48
|
-
use deepstrike_core::types::agent::AgentIdentity;
|
|
49
|
-
use deepstrike_core::types::policy::GovernanceVerdict as RustGovernanceVerdict;
|
|
50
|
-
use deepstrike_core::harness::eval_pipeline::{
|
|
51
|
-
Criterion as RustCriterion, EvalAction as RustEvalAction, EvalEvent as RustEvalEvent,
|
|
52
|
-
EvalPolicy as RustEvalPolicy, EvalPipeline as RustEvalPipeline,
|
|
53
|
-
};
|
|
54
|
-
use deepstrike_core::memory::curator::CurationResult as RustCurationResult;
|
|
55
|
-
use deepstrike_core::memory::durable::SessionData as RustSessionData;
|
|
56
|
-
use deepstrike_core::memory::idle_pipeline::{
|
|
57
|
-
IdleAction as RustIdleAction, IdleEvent as RustIdleEvent, IdlePolicy as RustIdlePolicy,
|
|
58
|
-
IdlePipeline as RustIdlePipeline,
|
|
59
|
-
};
|
|
60
|
-
use deepstrike_core::memory::semantic::MemoryEntry as RustMemoryEntry;
|
|
61
|
-
use deepstrike_core::scheduler::policy::LoopPolicy as RustLoopPolicy;
|
|
62
|
-
use deepstrike_core::scheduler::state_machine::{
|
|
63
|
-
LoopAction as RustLoopAction, LoopEvent as RustLoopEvent,
|
|
64
|
-
LoopObservation as RustLoopObservation, LoopStateMachine as RustLoopStateMachine,
|
|
65
|
-
};
|
|
66
|
-
use deepstrike_core::signals::router::SignalRouter as RustSignalRouter;
|
|
67
|
-
use deepstrike_core::types::policy::SignalDisposition as RustSignalDisposition;
|
|
68
|
-
use deepstrike_core::types::signal::{
|
|
69
|
-
RuntimeSignal as RustRuntimeSignal, SignalSource as RustSignalSource,
|
|
70
|
-
SignalType as RustSignalType, Urgency as RustUrgency,
|
|
71
|
-
};
|
|
72
|
-
use deepstrike_core::types::message::{
|
|
73
|
-
Content, ContentPart, Message as RustMessage, Role, ToolCall as RustToolCall,
|
|
74
|
-
ToolResult as RustToolResult, ToolSchema as RustToolSchema,
|
|
75
|
-
};
|
|
76
|
-
use deepstrike_core::types::result::LoopResult as RustLoopResult;
|
|
77
|
-
use deepstrike_core::types::skill::SkillMetadata as RustSkillMetadata;
|
|
78
|
-
use deepstrike_core::types::task::RuntimeTask as RustRuntimeTask;
|
|
79
|
-
|
|
80
|
-
// ────────────────────────────────────── POD types (plain JS objects) ──────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
#[napi(object)]
|
|
83
|
-
#[derive(Clone)]
|
|
84
|
-
pub struct ContentPartObj {
|
|
85
|
-
/// `"text"` | `"image"` | `"audio"` | `"tool_result"`
|
|
86
|
-
pub r#type: String,
|
|
87
|
-
pub text: Option<String>,
|
|
88
|
-
pub url: Option<String>,
|
|
89
|
-
pub data: Option<String>,
|
|
90
|
-
pub media_type: Option<String>,
|
|
91
|
-
pub detail: Option<String>,
|
|
92
|
-
pub call_id: Option<String>,
|
|
93
|
-
pub output: Option<String>,
|
|
94
|
-
pub is_error: Option<bool>,
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
#[napi(object)]
|
|
98
|
-
#[derive(Clone)]
|
|
99
|
-
pub struct Message {
|
|
100
|
-
pub role: String,
|
|
101
|
-
/// Plain-text content. When `content_parts` is present, this holds only the
|
|
102
|
-
/// concatenated text segments for backward compatibility.
|
|
103
|
-
pub content: String,
|
|
104
|
-
/// Structured multimodal content parts. When present, takes precedence over `content`.
|
|
105
|
-
pub content_parts: Option<Vec<ContentPartObj>>,
|
|
106
|
-
pub token_count: Option<u32>,
|
|
107
|
-
pub tool_calls: Vec<ToolCall>,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#[napi(object)]
|
|
111
|
-
#[derive(Clone)]
|
|
112
|
-
pub struct ToolCall {
|
|
113
|
-
pub id: String,
|
|
114
|
-
pub name: String,
|
|
115
|
-
/// JSON-encoded arguments. JS: `JSON.stringify(args)`.
|
|
116
|
-
pub arguments: String,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
#[napi(object)]
|
|
120
|
-
#[derive(Clone)]
|
|
121
|
-
pub struct ToolResult {
|
|
122
|
-
pub call_id: String,
|
|
123
|
-
pub output: String,
|
|
124
|
-
pub is_error: bool,
|
|
125
|
-
pub token_count: Option<u32>,
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
#[napi(object)]
|
|
129
|
-
#[derive(Clone)]
|
|
130
|
-
pub struct ToolSchema {
|
|
131
|
-
pub name: String,
|
|
132
|
-
pub description: String,
|
|
133
|
-
/// JSON-encoded JSON Schema. JS: `JSON.stringify(schema)`.
|
|
134
|
-
pub parameters: String,
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
#[napi(object)]
|
|
138
|
-
#[derive(Clone)]
|
|
139
|
-
pub struct RuntimeTask {
|
|
140
|
-
pub goal: String,
|
|
141
|
-
pub criteria: Option<Vec<String>>,
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
#[napi(object)]
|
|
145
|
-
#[derive(Clone)]
|
|
146
|
-
pub struct LoopPolicy {
|
|
147
|
-
pub max_tokens: u32,
|
|
148
|
-
pub max_turns: Option<u32>,
|
|
149
|
-
pub max_total_tokens: Option<BigInt>,
|
|
150
|
-
pub timeout_ms: Option<BigInt>,
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
#[napi(object)]
|
|
154
|
-
#[derive(Clone)]
|
|
155
|
-
pub struct LoopResult {
|
|
156
|
-
pub termination: String,
|
|
157
|
-
pub final_message: Option<Message>,
|
|
158
|
-
pub turns_used: u32,
|
|
159
|
-
pub total_tokens_used: BigInt,
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ────────────────────────────────────── Skill types ──────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
// ────────────────────────────────────── Signal types ──────────────────────────────────────
|
|
165
|
-
|
|
166
|
-
/// Unified RuntimeSignal exposed to Node.js — mirrors the kernel type.
|
|
167
|
-
#[napi(object)]
|
|
168
|
-
#[derive(Clone)]
|
|
169
|
-
pub struct RuntimeSignal {
|
|
170
|
-
pub id: String,
|
|
171
|
-
/// "cron" | "gateway" | "heartbeat" | "custom"
|
|
172
|
-
pub source: String,
|
|
173
|
-
/// "event" | "job" | "alert"
|
|
174
|
-
pub signal_type: String,
|
|
175
|
-
/// "low" | "normal" | "high" | "critical"
|
|
176
|
-
pub urgency: String,
|
|
177
|
-
pub summary: String,
|
|
178
|
-
/// JSON-encoded payload.
|
|
179
|
-
pub payload: String,
|
|
180
|
-
pub dedupe_key: Option<String>,
|
|
181
|
-
pub timestamp_ms: f64,
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
fn runtime_signal_to_rust(s: RuntimeSignal) -> Result<RustRuntimeSignal> {
|
|
185
|
-
let source = match s.source.as_str() {
|
|
186
|
-
"cron" => RustSignalSource::Cron,
|
|
187
|
-
"gateway" => RustSignalSource::Gateway,
|
|
188
|
-
"heartbeat" => RustSignalSource::Heartbeat,
|
|
189
|
-
_ => RustSignalSource::Custom,
|
|
190
|
-
};
|
|
191
|
-
let signal_type = match s.signal_type.as_str() {
|
|
192
|
-
"job" => RustSignalType::Job,
|
|
193
|
-
"alert" => RustSignalType::Alert,
|
|
194
|
-
_ => RustSignalType::Event,
|
|
195
|
-
};
|
|
196
|
-
let urgency = match s.urgency.as_str() {
|
|
197
|
-
"critical" => RustUrgency::Critical,
|
|
198
|
-
"high" => RustUrgency::High,
|
|
199
|
-
"low" => RustUrgency::Low,
|
|
200
|
-
_ => RustUrgency::Normal,
|
|
201
|
-
};
|
|
202
|
-
let payload: serde_json::Value = serde_json::from_str(&s.payload)
|
|
203
|
-
.unwrap_or(serde_json::Value::Null);
|
|
204
|
-
let mut sig = RustRuntimeSignal::new(source, signal_type, urgency, s.summary.as_str())
|
|
205
|
-
.with_payload(payload)
|
|
206
|
-
.with_timestamp(s.timestamp_ms as u64);
|
|
207
|
-
if let Some(key) = s.dedupe_key {
|
|
208
|
-
sig = sig.with_dedupe(key.as_str());
|
|
209
|
-
}
|
|
210
|
-
Ok(sig)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
fn runtime_signal_from_rust(s: &RustRuntimeSignal) -> RuntimeSignal {
|
|
214
|
-
let source = match s.source {
|
|
215
|
-
RustSignalSource::Cron => "cron",
|
|
216
|
-
RustSignalSource::Gateway => "gateway",
|
|
217
|
-
RustSignalSource::Heartbeat => "heartbeat",
|
|
218
|
-
RustSignalSource::Custom => "custom",
|
|
219
|
-
};
|
|
220
|
-
let signal_type = match s.signal_type {
|
|
221
|
-
RustSignalType::Event => "event",
|
|
222
|
-
RustSignalType::Job => "job",
|
|
223
|
-
RustSignalType::Alert => "alert",
|
|
224
|
-
};
|
|
225
|
-
let urgency = match s.urgency {
|
|
226
|
-
RustUrgency::Critical => "critical",
|
|
227
|
-
RustUrgency::High => "high",
|
|
228
|
-
RustUrgency::Normal => "normal",
|
|
229
|
-
RustUrgency::Low => "low",
|
|
230
|
-
};
|
|
231
|
-
RuntimeSignal {
|
|
232
|
-
id: s.id.to_string(),
|
|
233
|
-
source: source.into(),
|
|
234
|
-
signal_type: signal_type.into(),
|
|
235
|
-
urgency: urgency.into(),
|
|
236
|
-
summary: s.summary.to_string(),
|
|
237
|
-
payload: serde_json::to_string(&s.payload).unwrap_or_else(|_| "null".into()),
|
|
238
|
-
dedupe_key: s.dedupe_key.as_ref().map(|k| k.to_string()),
|
|
239
|
-
timestamp_ms: s.timestamp_ms as f64,
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
fn disposition_to_str(d: RustSignalDisposition) -> &'static str {
|
|
244
|
-
match d {
|
|
245
|
-
RustSignalDisposition::Ignore => "ignore",
|
|
246
|
-
RustSignalDisposition::Observe => "observe",
|
|
247
|
-
RustSignalDisposition::Queue => "queue",
|
|
248
|
-
RustSignalDisposition::Run { .. } => "run",
|
|
249
|
-
RustSignalDisposition::Interrupt => "interrupt",
|
|
250
|
-
RustSignalDisposition::InterruptNow => "interrupt_now",
|
|
251
|
-
RustSignalDisposition::Dropped => "dropped",
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
#[napi(object)]
|
|
256
|
-
#[derive(Clone)]
|
|
257
|
-
pub struct SkillMetadata {
|
|
258
|
-
pub name: String,
|
|
259
|
-
pub description: String,
|
|
260
|
-
pub when_to_use: Option<String>,
|
|
261
|
-
pub allowed_tools: Option<Vec<String>>,
|
|
262
|
-
pub effort: Option<u8>,
|
|
263
|
-
pub estimated_tokens: u32,
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ────────────────────────────── Tagged unions: LoopAction / LoopObservation ──────────────────────────────
|
|
267
|
-
|
|
268
|
-
/// Discriminated union. Inspect `kind`:
|
|
269
|
-
/// - `"call_llm"` → `messages`, `tools` (includes `skill` meta-tool when skills are registered)
|
|
270
|
-
/// - `"execute_tools"` → `calls`
|
|
271
|
-
/// - `"done"` → `result`
|
|
272
|
-
#[napi(object)]
|
|
273
|
-
#[derive(Clone)]
|
|
274
|
-
pub struct LoopAction {
|
|
275
|
-
pub kind: String,
|
|
276
|
-
pub messages: Option<Vec<Message>>,
|
|
277
|
-
pub tools: Option<Vec<ToolSchema>>,
|
|
278
|
-
pub calls: Option<Vec<ToolCall>>,
|
|
279
|
-
pub result: Option<LoopResult>,
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/// Discriminated union for observations:
|
|
283
|
-
/// - `"compressed"` → `action`, `rho_after`
|
|
284
|
-
#[napi(object)]
|
|
285
|
-
#[derive(Clone)]
|
|
286
|
-
pub struct LoopObservation {
|
|
287
|
-
pub kind: String,
|
|
288
|
-
pub action: Option<String>,
|
|
289
|
-
pub rho_after: Option<f64>,
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ────────────────────────────────── conversion helpers ──────────────────────────────────
|
|
293
|
-
|
|
294
|
-
fn role_str_to_rust(role: &str) -> Result<Role> {
|
|
295
|
-
match role {
|
|
296
|
-
"system" => Ok(Role::System),
|
|
297
|
-
"user" => Ok(Role::User),
|
|
298
|
-
"assistant" => Ok(Role::Assistant),
|
|
299
|
-
"tool" => Ok(Role::Tool),
|
|
300
|
-
other => Err(Error::new(Status::InvalidArg, format!("invalid role: {other}"))),
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
fn role_to_str(role: Role) -> &'static str {
|
|
305
|
-
match role {
|
|
306
|
-
Role::System => "system",
|
|
307
|
-
Role::User => "user",
|
|
308
|
-
Role::Assistant => "assistant",
|
|
309
|
-
Role::Tool => "tool",
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
fn content_part_to_rust(p: ContentPartObj) -> ContentPart {
|
|
314
|
-
match p.r#type.as_str() {
|
|
315
|
-
"image" => ContentPart::Image {
|
|
316
|
-
url: p.url,
|
|
317
|
-
data: p.data,
|
|
318
|
-
media_type: p.media_type,
|
|
319
|
-
detail: p.detail,
|
|
320
|
-
},
|
|
321
|
-
"audio" => ContentPart::Audio {
|
|
322
|
-
data: p.data.unwrap_or_default(),
|
|
323
|
-
media_type: p.media_type.unwrap_or_else(|| "audio/wav".into()),
|
|
324
|
-
},
|
|
325
|
-
"tool_result" => ContentPart::ToolResult {
|
|
326
|
-
call_id: CompactString::new(&p.call_id.unwrap_or_default()),
|
|
327
|
-
output: p.output.unwrap_or_default(),
|
|
328
|
-
is_error: p.is_error.unwrap_or(false),
|
|
329
|
-
},
|
|
330
|
-
_ => ContentPart::Text { text: p.text.unwrap_or_default() },
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
fn content_part_from_rust(p: &ContentPart) -> ContentPartObj {
|
|
335
|
-
match p {
|
|
336
|
-
ContentPart::Text { text } => ContentPartObj {
|
|
337
|
-
r#type: "text".into(), text: Some(text.clone()),
|
|
338
|
-
url: None, data: None, media_type: None, detail: None,
|
|
339
|
-
call_id: None, output: None, is_error: None,
|
|
340
|
-
},
|
|
341
|
-
ContentPart::Image { url, data, media_type, detail } => ContentPartObj {
|
|
342
|
-
r#type: "image".into(), text: None,
|
|
343
|
-
url: url.clone(), data: data.clone(), media_type: media_type.clone(), detail: detail.clone(),
|
|
344
|
-
call_id: None, output: None, is_error: None,
|
|
345
|
-
},
|
|
346
|
-
ContentPart::Audio { data, media_type } => ContentPartObj {
|
|
347
|
-
r#type: "audio".into(), text: None, url: None,
|
|
348
|
-
data: Some(data.clone()), media_type: Some(media_type.clone()), detail: None,
|
|
349
|
-
call_id: None, output: None, is_error: None,
|
|
350
|
-
},
|
|
351
|
-
ContentPart::ToolResult { call_id, output, is_error } => ContentPartObj {
|
|
352
|
-
r#type: "tool_result".into(), text: None, url: None, data: None, media_type: None, detail: None,
|
|
353
|
-
call_id: Some(call_id.to_string()), output: Some(output.clone()), is_error: Some(*is_error),
|
|
354
|
-
},
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
fn message_to_rust(m: Message) -> Result<RustMessage> {
|
|
359
|
-
let role = role_str_to_rust(&m.role)?;
|
|
360
|
-
let tool_calls: Vec<RustToolCall> = m
|
|
361
|
-
.tool_calls
|
|
362
|
-
.into_iter()
|
|
363
|
-
.map(tool_call_to_rust)
|
|
364
|
-
.collect::<Result<_>>()?;
|
|
365
|
-
let content = match m.content_parts {
|
|
366
|
-
Some(parts) if !parts.is_empty() => Content::Parts(parts.into_iter().map(content_part_to_rust).collect()),
|
|
367
|
-
_ => Content::Text(m.content),
|
|
368
|
-
};
|
|
369
|
-
Ok(RustMessage { role, content, tool_calls, token_count: m.token_count })
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
fn message_from_rust(m: &RustMessage) -> Message {
|
|
373
|
-
let (content, content_parts) = match &m.content {
|
|
374
|
-
Content::Text(s) => (s.clone(), None),
|
|
375
|
-
Content::Parts(parts) => {
|
|
376
|
-
let text_only: String = parts.iter().filter_map(|p| match p {
|
|
377
|
-
ContentPart::Text { text } => Some(text.as_str()),
|
|
378
|
-
_ => None,
|
|
379
|
-
}).collect::<Vec<_>>().join("\n");
|
|
380
|
-
let objs: Vec<ContentPartObj> = parts.iter().map(content_part_from_rust).collect();
|
|
381
|
-
(text_only, Some(objs))
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
Message {
|
|
385
|
-
role: role_to_str(m.role).to_string(),
|
|
386
|
-
content,
|
|
387
|
-
content_parts,
|
|
388
|
-
token_count: m.token_count,
|
|
389
|
-
tool_calls: m.tool_calls.iter().map(tool_call_from_rust).collect(),
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
fn tool_call_to_rust(c: ToolCall) -> Result<RustToolCall> {
|
|
394
|
-
let args: serde_json::Value = serde_json::from_str(&c.arguments)
|
|
395
|
-
.map_err(|e| Error::new(Status::InvalidArg, format!("invalid JSON arguments: {e}")))?;
|
|
396
|
-
Ok(RustToolCall {
|
|
397
|
-
id: CompactString::new(&c.id),
|
|
398
|
-
name: CompactString::new(&c.name),
|
|
399
|
-
arguments: args,
|
|
400
|
-
})
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
fn tool_call_from_rust(c: &RustToolCall) -> ToolCall {
|
|
404
|
-
ToolCall {
|
|
405
|
-
id: c.id.to_string(),
|
|
406
|
-
name: c.name.to_string(),
|
|
407
|
-
arguments: serde_json::to_string(&c.arguments).unwrap_or_else(|_| "null".into()),
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
fn tool_result_to_rust(r: ToolResult) -> RustToolResult {
|
|
412
|
-
RustToolResult {
|
|
413
|
-
call_id: CompactString::new(&r.call_id),
|
|
414
|
-
output: Content::Text(r.output),
|
|
415
|
-
is_error: r.is_error,
|
|
416
|
-
token_count: r.token_count,
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
fn tool_schema_to_rust(t: ToolSchema) -> Result<RustToolSchema> {
|
|
421
|
-
let params: serde_json::Value = serde_json::from_str(&t.parameters)
|
|
422
|
-
.map_err(|e| Error::new(Status::InvalidArg, format!("invalid JSON parameters: {e}")))?;
|
|
423
|
-
Ok(RustToolSchema {
|
|
424
|
-
name: CompactString::new(&t.name),
|
|
425
|
-
description: t.description,
|
|
426
|
-
parameters: params,
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
fn tool_schema_from_rust(t: &RustToolSchema) -> ToolSchema {
|
|
431
|
-
ToolSchema {
|
|
432
|
-
name: t.name.to_string(),
|
|
433
|
-
description: t.description.clone(),
|
|
434
|
-
parameters: serde_json::to_string(&t.parameters).unwrap_or_else(|_| "null".into()),
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
fn skill_metadata_to_rust(s: SkillMetadata) -> RustSkillMetadata {
|
|
439
|
-
RustSkillMetadata {
|
|
440
|
-
name: CompactString::new(&s.name),
|
|
441
|
-
description: s.description,
|
|
442
|
-
when_to_use: s.when_to_use,
|
|
443
|
-
allowed_tools: s
|
|
444
|
-
.allowed_tools
|
|
445
|
-
.unwrap_or_default()
|
|
446
|
-
.iter()
|
|
447
|
-
.map(CompactString::new)
|
|
448
|
-
.collect(),
|
|
449
|
-
effort: s.effort,
|
|
450
|
-
estimated_tokens: s.estimated_tokens,
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
fn task_to_rust(t: RuntimeTask) -> RustRuntimeTask {
|
|
455
|
-
RustRuntimeTask {
|
|
456
|
-
goal: t.goal,
|
|
457
|
-
criteria: t.criteria.unwrap_or_default(),
|
|
458
|
-
metadata: serde_json::Value::Null,
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
fn policy_to_rust(p: LoopPolicy) -> RustLoopPolicy {
|
|
463
|
-
RustLoopPolicy {
|
|
464
|
-
max_tokens: p.max_tokens,
|
|
465
|
-
max_turns: p.max_turns.unwrap_or(25),
|
|
466
|
-
max_total_tokens: p
|
|
467
|
-
.max_total_tokens
|
|
468
|
-
.map(|b| b.get_u64().1)
|
|
469
|
-
.unwrap_or(1_000_000),
|
|
470
|
-
timeout_ms: p.timeout_ms.map(|b| b.get_u64().1),
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
fn pressure_action_str(a: PressureAction) -> &'static str {
|
|
475
|
-
match a {
|
|
476
|
-
PressureAction::None => "none",
|
|
477
|
-
PressureAction::SnipCompact => "snip_compact",
|
|
478
|
-
PressureAction::MicroCompact => "micro_compact",
|
|
479
|
-
PressureAction::ContextCollapse => "context_collapse",
|
|
480
|
-
PressureAction::AutoCompact => "auto_compact",
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
fn loop_result_from_rust(r: &RustLoopResult) -> LoopResult {
|
|
485
|
-
let termination = match r.termination {
|
|
486
|
-
deepstrike_core::types::result::TerminationReason::Completed => "completed",
|
|
487
|
-
deepstrike_core::types::result::TerminationReason::MaxTurns => "max_turns",
|
|
488
|
-
deepstrike_core::types::result::TerminationReason::TokenBudget => "token_budget",
|
|
489
|
-
deepstrike_core::types::result::TerminationReason::Timeout => "timeout",
|
|
490
|
-
deepstrike_core::types::result::TerminationReason::UserAbort => "user_abort",
|
|
491
|
-
deepstrike_core::types::result::TerminationReason::Error => "error",
|
|
492
|
-
};
|
|
493
|
-
LoopResult {
|
|
494
|
-
termination: termination.to_string(),
|
|
495
|
-
final_message: r.final_message.as_ref().map(message_from_rust),
|
|
496
|
-
turns_used: r.turns_used,
|
|
497
|
-
total_tokens_used: BigInt::from(r.total_tokens_used),
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
fn loop_action_from_rust(a: RustLoopAction) -> LoopAction {
|
|
502
|
-
match a {
|
|
503
|
-
RustLoopAction::CallLLM { messages, tools } => LoopAction {
|
|
504
|
-
kind: "call_llm".into(),
|
|
505
|
-
messages: Some(messages.iter().map(message_from_rust).collect()),
|
|
506
|
-
tools: Some(tools.iter().map(tool_schema_from_rust).collect()),
|
|
507
|
-
calls: None,
|
|
508
|
-
result: None,
|
|
509
|
-
},
|
|
510
|
-
RustLoopAction::ExecuteTools { calls } => LoopAction {
|
|
511
|
-
kind: "execute_tools".into(),
|
|
512
|
-
messages: None,
|
|
513
|
-
tools: None,
|
|
514
|
-
calls: Some(calls.iter().map(tool_call_from_rust).collect()),
|
|
515
|
-
result: None,
|
|
516
|
-
},
|
|
517
|
-
RustLoopAction::Done { result } => LoopAction {
|
|
518
|
-
kind: "done".into(),
|
|
519
|
-
messages: None,
|
|
520
|
-
tools: None,
|
|
521
|
-
calls: None,
|
|
522
|
-
result: Some(loop_result_from_rust(&result)),
|
|
523
|
-
},
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
fn observation_from_rust(o: RustLoopObservation) -> LoopObservation {
|
|
528
|
-
match o {
|
|
529
|
-
RustLoopObservation::Compressed { action, rho_after } => LoopObservation {
|
|
530
|
-
kind: "compressed".into(),
|
|
531
|
-
action: Some(pressure_action_str(action).into()),
|
|
532
|
-
rho_after: Some(rho_after),
|
|
533
|
-
},
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ─────────────────────────────────────────── ContextEngine ───────────────────────────────────────────
|
|
538
|
-
|
|
539
|
-
#[napi]
|
|
540
|
-
pub struct ContextEngine {
|
|
541
|
-
inner: ContextManager,
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
#[napi]
|
|
545
|
-
impl ContextEngine {
|
|
546
|
-
#[napi(constructor)]
|
|
547
|
-
pub fn new(max_tokens: u32) -> Self {
|
|
548
|
-
Self { inner: ContextManager::new(max_tokens) }
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
#[napi]
|
|
552
|
-
pub fn add_system_message(&mut self, content: String, tokens: u32) {
|
|
553
|
-
self.inner
|
|
554
|
-
.partitions
|
|
555
|
-
.system
|
|
556
|
-
.push(RustMessage::system(content), tokens);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
#[napi]
|
|
560
|
-
pub fn add_user_message(&mut self, content: String, tokens: u32) {
|
|
561
|
-
self.inner.push_history(RustMessage::user(content), tokens);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
#[napi]
|
|
565
|
-
pub fn add_assistant_message(&mut self, content: String, tokens: u32) {
|
|
566
|
-
self.inner
|
|
567
|
-
.push_history(RustMessage::assistant(content), tokens);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
#[napi]
|
|
571
|
-
pub fn pressure(&self) -> f64 {
|
|
572
|
-
self.inner.rho()
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
#[napi]
|
|
576
|
-
pub fn total_tokens(&self) -> u32 {
|
|
577
|
-
self.inner.partitions.total_tokens()
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/// Run compression at the level the current pressure recommends.
|
|
581
|
-
/// Returns tokens saved.
|
|
582
|
-
#[napi]
|
|
583
|
-
pub fn compress(&mut self) -> u32 {
|
|
584
|
-
let action = self.inner.should_compress();
|
|
585
|
-
if action == PressureAction::None {
|
|
586
|
-
return 0;
|
|
587
|
-
}
|
|
588
|
-
let before = self.inner.partitions.total_tokens();
|
|
589
|
-
self.inner.compress(action);
|
|
590
|
-
let after = self.inner.partitions.total_tokens();
|
|
591
|
-
before.saturating_sub(after)
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
#[napi]
|
|
595
|
-
pub fn render(&self) -> Vec<Message> {
|
|
596
|
-
self.inner.render().iter().map(message_from_rust).collect()
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/// Replace the available-skills set with frontmatter-only metadata.
|
|
600
|
-
/// The kernel will auto-inject the `skill` meta-tool into every `CallLLM` action.
|
|
601
|
-
#[napi]
|
|
602
|
-
pub fn set_available_skills(&mut self, skills: Vec<SkillMetadata>) {
|
|
603
|
-
let rust_skills = skills.into_iter().map(skill_metadata_to_rust).collect();
|
|
604
|
-
self.inner.set_available_skills(rust_skills);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// ─────────────────────────────────────────── LoopStateMachine ───────────────────────────────────────────
|
|
609
|
-
|
|
610
|
-
#[napi]
|
|
611
|
-
pub struct LoopStateMachine {
|
|
612
|
-
inner: RustLoopStateMachine,
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
#[napi]
|
|
616
|
-
impl LoopStateMachine {
|
|
617
|
-
#[napi(constructor)]
|
|
618
|
-
pub fn new(policy: LoopPolicy) -> Self {
|
|
619
|
-
Self { inner: RustLoopStateMachine::new(policy_to_rust(policy)) }
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/// Convenience: register skills directly on the state machine without
|
|
623
|
-
/// reaching into the inner ContextEngine.
|
|
624
|
-
#[napi]
|
|
625
|
-
pub fn set_available_skills(&mut self, skills: Vec<SkillMetadata>) {
|
|
626
|
-
let rust_skills = skills.into_iter().map(skill_metadata_to_rust).collect();
|
|
627
|
-
self.inner.ctx.set_available_skills(rust_skills);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/// Enable the `memory` meta-tool. Call with `true` when a DreamStore and agentId
|
|
631
|
-
/// are configured — the SDK layer intercepts `memory` tool calls and runs the search.
|
|
632
|
-
#[napi]
|
|
633
|
-
pub fn set_memory_enabled(&mut self, enabled: bool) {
|
|
634
|
-
self.inner.ctx.set_memory_enabled(enabled);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/// Enable the `knowledge` meta-tool. Call with `true` when a KnowledgeSource
|
|
638
|
-
/// is configured — the SDK layer intercepts `knowledge` tool calls and runs retrieval.
|
|
639
|
-
#[napi]
|
|
640
|
-
pub fn set_knowledge_enabled(&mut self, enabled: bool) {
|
|
641
|
-
self.inner.ctx.set_knowledge_enabled(enabled);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/// Prepend a system-level instruction to the context. Must be called before `start`.
|
|
645
|
-
/// `tokens` is a caller-supplied estimate (use `content.length / 4` if unsure).
|
|
646
|
-
/// The renderer skips messages with `tokens == 0`, so always pass at least 1.
|
|
647
|
-
#[napi]
|
|
648
|
-
pub fn add_system_message(&mut self, content: String, tokens: u32) {
|
|
649
|
-
self.inner
|
|
650
|
-
.ctx
|
|
651
|
-
.partitions
|
|
652
|
-
.system
|
|
653
|
-
.push(RustMessage::system(content), tokens.max(1));
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/// Pre-populate the memory partition with a long-term memory snippet.
|
|
657
|
-
/// Must be called before `start`. Use for seeding known context from past sessions.
|
|
658
|
-
/// `tokens` is a caller-supplied estimate; pass at least 1.
|
|
659
|
-
#[napi]
|
|
660
|
-
pub fn add_memory_message(&mut self, content: String, tokens: u32) {
|
|
661
|
-
self.inner
|
|
662
|
-
.ctx
|
|
663
|
-
.partitions
|
|
664
|
-
.memory
|
|
665
|
-
.push(RustMessage::user(content), tokens.max(1));
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
#[napi]
|
|
669
|
-
pub fn set_tools(&mut self, tools: Vec<ToolSchema>) -> Result<()> {
|
|
670
|
-
let rust_tools: Vec<RustToolSchema> = tools
|
|
671
|
-
.into_iter()
|
|
672
|
-
.map(tool_schema_to_rust)
|
|
673
|
-
.collect::<Result<_>>()?;
|
|
674
|
-
self.inner.tools = rust_tools;
|
|
675
|
-
Ok(())
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
#[napi]
|
|
679
|
-
pub fn start(&mut self, task: RuntimeTask) -> LoopAction {
|
|
680
|
-
loop_action_from_rust(self.inner.start(task_to_rust(task)))
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
#[napi]
|
|
684
|
-
pub fn feed_llm_response(&mut self, message: Message) -> Result<LoopAction> {
|
|
685
|
-
let msg = message_to_rust(message)?;
|
|
686
|
-
Ok(loop_action_from_rust(
|
|
687
|
-
self.inner.feed(RustLoopEvent::LLMResponse { message: msg }),
|
|
688
|
-
))
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
#[napi]
|
|
692
|
-
pub fn feed_tool_results(&mut self, results: Vec<ToolResult>) -> LoopAction {
|
|
693
|
-
let results: Vec<RustToolResult> = results.into_iter().map(tool_result_to_rust).collect();
|
|
694
|
-
loop_action_from_rust(self.inner.feed(RustLoopEvent::ToolResults { results }))
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
#[napi]
|
|
698
|
-
pub fn feed_timeout(&mut self) -> LoopAction {
|
|
699
|
-
loop_action_from_rust(self.inner.feed(RustLoopEvent::Timeout))
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
#[napi]
|
|
703
|
-
pub fn is_terminal(&self) -> bool {
|
|
704
|
-
self.inner.is_terminal()
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
#[napi]
|
|
708
|
-
pub fn turn(&self) -> u32 {
|
|
709
|
-
self.inner.turn
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
#[napi]
|
|
713
|
-
pub fn pressure(&self) -> f64 {
|
|
714
|
-
self.inner.ctx.rho()
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/// Drain observations emitted during the most recent feed call.
|
|
718
|
-
#[napi]
|
|
719
|
-
pub fn take_observations(&mut self) -> Vec<LoopObservation> {
|
|
720
|
-
self.inner
|
|
721
|
-
.take_observations()
|
|
722
|
-
.into_iter()
|
|
723
|
-
.map(observation_from_rust)
|
|
724
|
-
.collect()
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
#[napi]
|
|
728
|
-
pub fn render(&self) -> Vec<Message> {
|
|
729
|
-
self.inner.ctx.render().iter().map(message_from_rust).collect()
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// ─────────────────────────────────────────── SignalRouter ───────────────────────────────────────────
|
|
734
|
-
|
|
735
|
-
#[napi]
|
|
736
|
-
pub struct SignalRouter {
|
|
737
|
-
inner: RustSignalRouter,
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
#[napi]
|
|
741
|
-
impl SignalRouter {
|
|
742
|
-
#[napi(constructor)]
|
|
743
|
-
pub fn new(max_queue_size: u32) -> Self {
|
|
744
|
-
Self { inner: RustSignalRouter::new(max_queue_size as usize) }
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/// Ingest a signal. Returns the disposition string:
|
|
748
|
-
/// "ignore" | "observe" | "queue" | "run" | "interrupt" | "interrupt_now" | "dropped"
|
|
749
|
-
#[napi]
|
|
750
|
-
pub fn ingest(&mut self, signal: RuntimeSignal, is_running: bool) -> Result<String> {
|
|
751
|
-
let rust_sig = runtime_signal_to_rust(signal)?;
|
|
752
|
-
Ok(disposition_to_str(self.inner.ingest(rust_sig, is_running)).into())
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/// Pull the next queued signal (highest priority first).
|
|
756
|
-
#[napi]
|
|
757
|
-
pub fn next(&mut self) -> Option<RuntimeSignal> {
|
|
758
|
-
self.inner.next().as_ref().map(runtime_signal_from_rust)
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
#[napi]
|
|
762
|
-
pub fn depth(&self) -> u32 {
|
|
763
|
-
self.inner.depth() as u32
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
#[napi]
|
|
767
|
-
pub fn clear_dedup(&mut self) {
|
|
768
|
-
self.inner.clear_dedup();
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// ─────────────────────────────────────────── Governance ───────────────────────────────────────────
|
|
773
|
-
|
|
774
|
-
/// JS-friendly governance verdict returned by `Governance.evaluate`.
|
|
775
|
-
#[napi(object)]
|
|
776
|
-
#[derive(Clone)]
|
|
777
|
-
pub struct GovernanceVerdictObj {
|
|
778
|
-
/// `"allow"` | `"deny"` | `"rate_limited"` | `"ask_user"`
|
|
779
|
-
pub kind: String,
|
|
780
|
-
pub reason: Option<String>,
|
|
781
|
-
/// Milliseconds until the tool may be retried. Only set when `kind === "rate_limited"`.
|
|
782
|
-
pub retry_after_ms: Option<f64>,
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
fn verdict_to_js(v: RustGovernanceVerdict) -> GovernanceVerdictObj {
|
|
786
|
-
match v {
|
|
787
|
-
RustGovernanceVerdict::Allow => GovernanceVerdictObj { kind: "allow".into(), reason: None, retry_after_ms: None },
|
|
788
|
-
RustGovernanceVerdict::Deny { reason, .. } => GovernanceVerdictObj { kind: "deny".into(), reason: Some(reason), retry_after_ms: None },
|
|
789
|
-
RustGovernanceVerdict::RateLimited { retry_after_ms } => GovernanceVerdictObj { kind: "rate_limited".into(), reason: None, retry_after_ms: Some(retry_after_ms as f64) },
|
|
790
|
-
RustGovernanceVerdict::AskUser { reason } => GovernanceVerdictObj { kind: "ask_user".into(), reason: Some(reason), retry_after_ms: None },
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
#[napi]
|
|
795
|
-
pub struct Governance {
|
|
796
|
-
inner: RustGovernancePipeline,
|
|
797
|
-
agent_id: String,
|
|
798
|
-
session_id: String,
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
#[napi]
|
|
802
|
-
impl Governance {
|
|
803
|
-
/// Create a governance pipeline.
|
|
804
|
-
/// `defaultAction` controls the fallback when no rule matches: `"allow"` (default) or `"deny"`.
|
|
805
|
-
#[napi(constructor)]
|
|
806
|
-
pub fn new(default_action: Option<String>) -> Self {
|
|
807
|
-
let action = match default_action.as_deref() {
|
|
808
|
-
Some("deny") => PermissionAction::Deny,
|
|
809
|
-
Some("ask_user") => PermissionAction::AskUser,
|
|
810
|
-
_ => PermissionAction::Allow,
|
|
811
|
-
};
|
|
812
|
-
Self {
|
|
813
|
-
inner: RustGovernancePipeline::new(action),
|
|
814
|
-
agent_id: "anonymous".into(),
|
|
815
|
-
session_id: "".into(),
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/// Set the agent identity used in governance audit logs.
|
|
820
|
-
#[napi]
|
|
821
|
-
pub fn set_identity(&mut self, agent_id: String, session_id: String) {
|
|
822
|
-
self.agent_id = agent_id;
|
|
823
|
-
self.session_id = session_id;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/// Add a permission rule. `pattern` supports globs: `"db.*"`, `"*.delete"`, `"*"`, or exact names.
|
|
827
|
-
/// `action`: `"allow"` | `"deny"` | `"ask_user"`.
|
|
828
|
-
/// Rules are evaluated in insertion order; first match wins.
|
|
829
|
-
#[napi]
|
|
830
|
-
pub fn add_permission_rule(&mut self, pattern: String, action: String) {
|
|
831
|
-
let perm_action = match action.as_str() {
|
|
832
|
-
"deny" => PermissionAction::Deny,
|
|
833
|
-
"ask_user" => PermissionAction::AskUser,
|
|
834
|
-
_ => PermissionAction::Allow,
|
|
835
|
-
};
|
|
836
|
-
self.inner.permission.add_rule(PermissionRule {
|
|
837
|
-
tool_pattern: pattern.into(),
|
|
838
|
-
action: perm_action,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/// Hard-block a tool name (veto stage — cannot be overridden by permission rules).
|
|
843
|
-
#[napi]
|
|
844
|
-
pub fn block_tool(&mut self, name: String) {
|
|
845
|
-
self.inner.veto.block_tool(name);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/// Advance the internal clock used by rate limiting and audit.
|
|
849
|
-
#[napi]
|
|
850
|
-
pub fn set_time(&mut self, now_ms: BigInt) {
|
|
851
|
-
self.inner.set_time(now_ms.get_u64().1);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/// Evaluate a tool call through the full pipeline (Permission → Veto → RateLimit → Constraint → Audit).
|
|
855
|
-
/// `argsJson`: JSON-encoded tool arguments string.
|
|
856
|
-
#[napi]
|
|
857
|
-
pub fn evaluate(&mut self, tool_name: String, args_json: String) -> Result<GovernanceVerdictObj> {
|
|
858
|
-
let args: serde_json::Value = serde_json::from_str(&args_json)
|
|
859
|
-
.unwrap_or(serde_json::Value::Null);
|
|
860
|
-
let call = RustToolCall {
|
|
861
|
-
id: compact_str::CompactString::new(""),
|
|
862
|
-
name: compact_str::CompactString::new(&tool_name),
|
|
863
|
-
arguments: args,
|
|
864
|
-
};
|
|
865
|
-
let caller = AgentIdentity::new(self.agent_id.as_str(), self.session_id.as_str());
|
|
866
|
-
Ok(verdict_to_js(self.inner.evaluate(&call, &caller)))
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// ──────────────────────────────── Dream / idle-pipeline POD types ────────────────────────────────
|
|
871
|
-
|
|
872
|
-
/// A single session of agent messages, used as input to `IdlePipeline.feedTrigger`.
|
|
873
|
-
#[napi(object)]
|
|
874
|
-
#[derive(Clone)]
|
|
875
|
-
pub struct SessionData {
|
|
876
|
-
pub session_id: String,
|
|
877
|
-
pub agent_id: String,
|
|
878
|
-
/// Messages from this session.
|
|
879
|
-
pub messages: Vec<Message>,
|
|
880
|
-
/// JSON-encoded metadata blob.
|
|
881
|
-
pub metadata: String,
|
|
882
|
-
/// Unix ms timestamp.
|
|
883
|
-
pub created_at_ms: f64,
|
|
884
|
-
/// Unix ms timestamp.
|
|
885
|
-
pub updated_at_ms: f64,
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/// A long-term memory entry as stored by the agent.
|
|
889
|
-
#[napi(object)]
|
|
890
|
-
#[derive(Clone)]
|
|
891
|
-
pub struct MemoryEntry {
|
|
892
|
-
pub text: String,
|
|
893
|
-
pub score: f64,
|
|
894
|
-
/// JSON-encoded metadata blob.
|
|
895
|
-
pub metadata: String,
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
#[napi(object)]
|
|
899
|
-
#[derive(Clone)]
|
|
900
|
-
pub struct CurationStats {
|
|
901
|
-
pub insights_processed: u32,
|
|
902
|
-
pub duplicates_removed: u32,
|
|
903
|
-
pub conflicts_resolved: u32,
|
|
904
|
-
pub entries_added: u32,
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/// The delta the `DreamStore.commit` must apply: add `toAdd`, remove `toRemoveIndices`.
|
|
908
|
-
#[napi(object)]
|
|
909
|
-
#[derive(Clone)]
|
|
910
|
-
pub struct CurationResult {
|
|
911
|
-
pub to_add: Vec<MemoryEntry>,
|
|
912
|
-
/// Indices into the `existingMemories` slice passed to `feedTrigger`.
|
|
913
|
-
pub to_remove_indices: Vec<u32>,
|
|
914
|
-
pub stats: CurationStats,
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
#[napi(object)]
|
|
918
|
-
#[derive(Clone)]
|
|
919
|
-
pub struct IdleRunResult {
|
|
920
|
-
pub sessions_processed: u32,
|
|
921
|
-
pub insights_extracted: u32,
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/// Discriminated union returned by `IdlePipeline` methods. Inspect `kind`:
|
|
925
|
-
/// - `"synthesize_insights"` → `messages` (SDK must call LLM, then `feedSynthesisResult`)
|
|
926
|
-
/// - `"commit_memories"` → `agentId`, `curationResult`, `runResult`
|
|
927
|
-
/// - `"noop"` | `"aborted"`
|
|
928
|
-
#[napi(object)]
|
|
929
|
-
#[derive(Clone)]
|
|
930
|
-
pub struct IdlePipelineAction {
|
|
931
|
-
pub kind: String,
|
|
932
|
-
pub messages: Option<Vec<Message>>,
|
|
933
|
-
pub agent_id: Option<String>,
|
|
934
|
-
pub curation_result: Option<CurationResult>,
|
|
935
|
-
pub run_result: Option<IdleRunResult>,
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// ─────────────────────── Dream conversion helpers ───────────────────────
|
|
939
|
-
|
|
940
|
-
fn session_data_to_rust(s: SessionData) -> Result<RustSessionData> {
|
|
941
|
-
let messages: Vec<RustMessage> =
|
|
942
|
-
s.messages.into_iter().map(message_to_rust).collect::<Result<_>>()?;
|
|
943
|
-
let metadata: serde_json::Value =
|
|
944
|
-
serde_json::from_str(&s.metadata).unwrap_or(serde_json::Value::Null);
|
|
945
|
-
Ok(RustSessionData {
|
|
946
|
-
session_id: s.session_id,
|
|
947
|
-
agent_id: s.agent_id,
|
|
948
|
-
messages,
|
|
949
|
-
metadata,
|
|
950
|
-
created_at_ms: s.created_at_ms as u64,
|
|
951
|
-
updated_at_ms: s.updated_at_ms as u64,
|
|
952
|
-
})
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
fn memory_entry_to_rust(e: MemoryEntry) -> RustMemoryEntry {
|
|
956
|
-
let metadata: serde_json::Value =
|
|
957
|
-
serde_json::from_str(&e.metadata).unwrap_or(serde_json::Value::Null);
|
|
958
|
-
RustMemoryEntry { text: e.text, score: e.score, metadata }
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
fn memory_entry_from_rust(e: &RustMemoryEntry) -> MemoryEntry {
|
|
962
|
-
MemoryEntry {
|
|
963
|
-
text: e.text.clone(),
|
|
964
|
-
score: e.score,
|
|
965
|
-
metadata: serde_json::to_string(&e.metadata).unwrap_or_else(|_| "null".into()),
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
fn curation_result_from_rust(r: RustCurationResult) -> CurationResult {
|
|
970
|
-
CurationResult {
|
|
971
|
-
to_add: r.to_add.iter().map(memory_entry_from_rust).collect(),
|
|
972
|
-
to_remove_indices: r.to_remove_indices.iter().map(|&i| i as u32).collect(),
|
|
973
|
-
stats: CurationStats {
|
|
974
|
-
insights_processed: r.stats.insights_processed as u32,
|
|
975
|
-
duplicates_removed: r.stats.duplicates_removed as u32,
|
|
976
|
-
conflicts_resolved: r.stats.conflicts_resolved as u32,
|
|
977
|
-
entries_added: r.stats.entries_added as u32,
|
|
978
|
-
},
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
fn idle_pipeline_action_from_rust(a: RustIdleAction) -> IdlePipelineAction {
|
|
983
|
-
match a {
|
|
984
|
-
RustIdleAction::SynthesizeInsights { messages } => IdlePipelineAction {
|
|
985
|
-
kind: "synthesize_insights".into(),
|
|
986
|
-
messages: Some(messages.iter().map(message_from_rust).collect()),
|
|
987
|
-
agent_id: None,
|
|
988
|
-
curation_result: None,
|
|
989
|
-
run_result: None,
|
|
990
|
-
},
|
|
991
|
-
RustIdleAction::CommitMemories { agent_id, result, run_result } => IdlePipelineAction {
|
|
992
|
-
kind: "commit_memories".into(),
|
|
993
|
-
messages: None,
|
|
994
|
-
agent_id: Some(agent_id),
|
|
995
|
-
curation_result: Some(curation_result_from_rust(result)),
|
|
996
|
-
run_result: Some(IdleRunResult {
|
|
997
|
-
sessions_processed: run_result.sessions_processed as u32,
|
|
998
|
-
insights_extracted: run_result.insights_extracted as u32,
|
|
999
|
-
}),
|
|
1000
|
-
},
|
|
1001
|
-
RustIdleAction::Noop => IdlePipelineAction {
|
|
1002
|
-
kind: "noop".into(),
|
|
1003
|
-
messages: None,
|
|
1004
|
-
agent_id: None,
|
|
1005
|
-
curation_result: None,
|
|
1006
|
-
run_result: None,
|
|
1007
|
-
},
|
|
1008
|
-
RustIdleAction::Aborted => IdlePipelineAction {
|
|
1009
|
-
kind: "aborted".into(),
|
|
1010
|
-
messages: None,
|
|
1011
|
-
agent_id: None,
|
|
1012
|
-
curation_result: None,
|
|
1013
|
-
run_result: None,
|
|
1014
|
-
},
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// ─────────────────────────────────────────── EvalPipeline ────────────────────────────────────────
|
|
1019
|
-
|
|
1020
|
-
#[napi(object)]
|
|
1021
|
-
#[derive(Clone)]
|
|
1022
|
-
pub struct Criterion {
|
|
1023
|
-
pub text: String,
|
|
1024
|
-
pub required: bool,
|
|
1025
|
-
pub weight: Option<f64>,
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
#[napi(object)]
|
|
1029
|
-
#[derive(Clone)]
|
|
1030
|
-
pub struct CriterionResult {
|
|
1031
|
-
pub criterion: String,
|
|
1032
|
-
pub passed: bool,
|
|
1033
|
-
pub score: f64,
|
|
1034
|
-
pub feedback: String,
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
#[napi(object)]
|
|
1038
|
-
#[derive(Clone)]
|
|
1039
|
-
pub struct EvalPipelineOptions {
|
|
1040
|
-
pub extract_skill_on_pass: Option<bool>,
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
#[napi(object)]
|
|
1044
|
-
#[derive(Clone)]
|
|
1045
|
-
pub struct SkillCandidate {
|
|
1046
|
-
pub name: String,
|
|
1047
|
-
pub description: String,
|
|
1048
|
-
pub when_to_use: Option<String>,
|
|
1049
|
-
pub content: String,
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
/// Discriminated union returned by `EvalPipeline` methods. Inspect `kind`:
|
|
1053
|
-
/// - `"evaluate"` → `messages` (SDK must call evaluator LLM, then `feedEvalResult`)
|
|
1054
|
-
/// - `"done"` → `passed`, `overallScore`, `feedback`, `details`, optional `skillCandidate`
|
|
1055
|
-
#[napi(object)]
|
|
1056
|
-
#[derive(Clone)]
|
|
1057
|
-
pub struct EvalPipelineAction {
|
|
1058
|
-
pub kind: String,
|
|
1059
|
-
pub messages: Option<Vec<Message>>,
|
|
1060
|
-
pub passed: Option<bool>,
|
|
1061
|
-
pub overall_score: Option<f64>,
|
|
1062
|
-
pub feedback: Option<String>,
|
|
1063
|
-
pub details: Option<Vec<CriterionResult>>,
|
|
1064
|
-
pub skill_candidate: Option<SkillCandidate>,
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
/// Kernel state machine for the evaluation cycle.
|
|
1068
|
-
///
|
|
1069
|
-
/// Drive it like this:
|
|
1070
|
-
/// 1. `feedOutcome(goal, criteria, result, attempt)` → `"evaluate"` action
|
|
1071
|
-
/// 2. Call evaluator LLM with `action.messages`, collect the text response
|
|
1072
|
-
/// 3. `feedEvalResult(text)` → `"done"` action
|
|
1073
|
-
/// 4. Read `action.passed` / `action.feedback` / `action.skillCandidate`
|
|
1074
|
-
/// 5. Call `reset()` before the next attempt
|
|
1075
|
-
#[napi]
|
|
1076
|
-
pub struct EvalPipeline {
|
|
1077
|
-
inner: RustEvalPipeline,
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
#[napi]
|
|
1081
|
-
impl EvalPipeline {
|
|
1082
|
-
#[napi(constructor)]
|
|
1083
|
-
pub fn new(options: Option<EvalPipelineOptions>) -> Self {
|
|
1084
|
-
let policy = RustEvalPolicy {
|
|
1085
|
-
extract_skill_on_pass: options
|
|
1086
|
-
.and_then(|o| o.extract_skill_on_pass)
|
|
1087
|
-
.unwrap_or(true),
|
|
1088
|
-
};
|
|
1089
|
-
Self { inner: RustEvalPipeline::new(policy) }
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
/// Phase 1 — provide the goal, criteria, agent output, and attempt number.
|
|
1093
|
-
/// Returns an `"evaluate"` action with messages to send to the evaluator LLM.
|
|
1094
|
-
#[napi]
|
|
1095
|
-
pub fn feed_outcome(
|
|
1096
|
-
&mut self,
|
|
1097
|
-
goal: String,
|
|
1098
|
-
criteria: Vec<Criterion>,
|
|
1099
|
-
result: String,
|
|
1100
|
-
attempt: u32,
|
|
1101
|
-
) -> EvalPipelineAction {
|
|
1102
|
-
let rust_criteria = criteria.into_iter().map(|c| RustCriterion {
|
|
1103
|
-
text: c.text,
|
|
1104
|
-
required: c.required,
|
|
1105
|
-
weight: c.weight.map(|w| w as f32).unwrap_or(1.0),
|
|
1106
|
-
}).collect();
|
|
1107
|
-
match self.inner.feed(RustEvalEvent::Outcome { goal, criteria: rust_criteria, result, attempt }) {
|
|
1108
|
-
RustEvalAction::Evaluate { messages } => EvalPipelineAction {
|
|
1109
|
-
kind: "evaluate".into(),
|
|
1110
|
-
messages: Some(messages.iter().map(message_from_rust).collect()),
|
|
1111
|
-
passed: None,
|
|
1112
|
-
overall_score: None,
|
|
1113
|
-
feedback: None,
|
|
1114
|
-
details: None,
|
|
1115
|
-
skill_candidate: None,
|
|
1116
|
-
},
|
|
1117
|
-
RustEvalAction::Done { result } => eval_done_action(result),
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
/// Phase 2 — feed back the evaluator LLM's text response.
|
|
1122
|
-
#[napi]
|
|
1123
|
-
pub fn feed_eval_result(&mut self, content: String) -> EvalPipelineAction {
|
|
1124
|
-
match self.inner.feed(RustEvalEvent::EvalResult { content }) {
|
|
1125
|
-
RustEvalAction::Done { result } => eval_done_action(result),
|
|
1126
|
-
RustEvalAction::Evaluate { messages } => EvalPipelineAction {
|
|
1127
|
-
kind: "evaluate".into(),
|
|
1128
|
-
messages: Some(messages.iter().map(message_from_rust).collect()),
|
|
1129
|
-
passed: None,
|
|
1130
|
-
overall_score: None,
|
|
1131
|
-
feedback: None,
|
|
1132
|
-
details: None,
|
|
1133
|
-
skill_candidate: None,
|
|
1134
|
-
},
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
#[napi]
|
|
1139
|
-
pub fn reset(&mut self) {
|
|
1140
|
-
self.inner.reset();
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
#[napi]
|
|
1144
|
-
pub fn is_idle(&self) -> bool {
|
|
1145
|
-
self.inner.is_idle()
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
fn eval_done_action(result: deepstrike_core::harness::eval_pipeline::EvalResult) -> EvalPipelineAction {
|
|
1150
|
-
EvalPipelineAction {
|
|
1151
|
-
kind: "done".into(),
|
|
1152
|
-
messages: None,
|
|
1153
|
-
passed: Some(result.passed),
|
|
1154
|
-
overall_score: Some(result.overall_score as f64),
|
|
1155
|
-
feedback: Some(result.feedback),
|
|
1156
|
-
details: Some(result.details.into_iter().map(|d| CriterionResult {
|
|
1157
|
-
criterion: d.criterion,
|
|
1158
|
-
passed: d.passed,
|
|
1159
|
-
score: d.score as f64,
|
|
1160
|
-
feedback: d.feedback,
|
|
1161
|
-
}).collect()),
|
|
1162
|
-
skill_candidate: result.skill_candidate.map(|s| SkillCandidate {
|
|
1163
|
-
name: s.name,
|
|
1164
|
-
description: s.description,
|
|
1165
|
-
when_to_use: s.when_to_use,
|
|
1166
|
-
content: s.content,
|
|
1167
|
-
}),
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/// Kernel state machine for the idle dreaming cycle.
|
|
1172
|
-
///
|
|
1173
|
-
/// Drive it like this:
|
|
1174
|
-
/// 1. `feedTrigger(sessions, existingMemories, nowMs)` → `"synthesize_insights"` action
|
|
1175
|
-
/// 2. Call LLM with `action.messages`, collect the text response
|
|
1176
|
-
/// 3. `feedSynthesisResult(text)` → `"commit_memories"` action
|
|
1177
|
-
/// 4. Apply `action.curationResult` via `DreamStore.commit`, then call `reset()`
|
|
1178
|
-
#[napi]
|
|
1179
|
-
pub struct IdlePipeline {
|
|
1180
|
-
inner: RustIdlePipeline,
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
#[napi]
|
|
1184
|
-
impl IdlePipeline {
|
|
1185
|
-
#[napi(constructor)]
|
|
1186
|
-
pub fn new(agent_id: String) -> Self {
|
|
1187
|
-
Self { inner: RustIdlePipeline::new(RustIdlePolicy::new(agent_id)) }
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
/// Phase 1 — provide sessions + current memory snapshot; kernel builds the LLM prompt.
|
|
1191
|
-
#[napi]
|
|
1192
|
-
pub fn feed_trigger(
|
|
1193
|
-
&mut self,
|
|
1194
|
-
sessions: Vec<SessionData>,
|
|
1195
|
-
existing_memories: Vec<MemoryEntry>,
|
|
1196
|
-
now_ms: f64,
|
|
1197
|
-
) -> Result<IdlePipelineAction> {
|
|
1198
|
-
let rust_sessions: Vec<RustSessionData> =
|
|
1199
|
-
sessions.into_iter().map(session_data_to_rust).collect::<Result<_>>()?;
|
|
1200
|
-
let rust_memories: Vec<RustMemoryEntry> =
|
|
1201
|
-
existing_memories.into_iter().map(memory_entry_to_rust).collect();
|
|
1202
|
-
let action = self.inner.feed(RustIdleEvent::Trigger {
|
|
1203
|
-
sessions: rust_sessions,
|
|
1204
|
-
existing_memories: rust_memories,
|
|
1205
|
-
now_ms: now_ms as u64,
|
|
1206
|
-
});
|
|
1207
|
-
Ok(idle_pipeline_action_from_rust(action))
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
/// Phase 2 — feed back the LLM's synthesis text; kernel parses and curates.
|
|
1211
|
-
#[napi]
|
|
1212
|
-
pub fn feed_synthesis_result(&mut self, content: String) -> IdlePipelineAction {
|
|
1213
|
-
idle_pipeline_action_from_rust(self.inner.feed(RustIdleEvent::SynthesisResult { content }))
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
#[napi]
|
|
1217
|
-
pub fn is_idle(&self) -> bool {
|
|
1218
|
-
self.inner.is_idle()
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
/// Reset to `Idle` after handling `CommitMemories` to allow the next cycle.
|
|
1222
|
-
#[napi]
|
|
1223
|
-
pub fn reset(&mut self) {
|
|
1224
|
-
self.inner.reset();
|
|
1225
|
-
}
|
|
1226
|
-
}
|