@deepstrike/core 0.1.0

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/Cargo.toml ADDED
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "deepstrike-node"
3
+ description = "Node.js bindings for DeepStrike runtime kernel"
4
+ version.workspace = true
5
+ edition.workspace = true
6
+ license.workspace = true
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ # napi6 is required for BigInt support (we use it for u64 token counts and timestamps)
13
+ napi = { version = "2", default-features = false, features = ["napi6"] }
14
+ napi-derive = "2"
15
+ deepstrike-core = { workspace = true }
16
+ deepstrike-tokenizer = { workspace = true }
17
+ compact_str = { workspace = true }
18
+ serde_json = { workspace = true }
19
+
20
+ [build-dependencies]
21
+ napi-build = "2"
22
+
23
+ [profile.release]
24
+ lto = true
package/build.rs ADDED
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ napi_build::setup();
3
+ }
package/index.d.ts ADDED
File without changes
package/index.js ADDED
@@ -0,0 +1,72 @@
1
+ /* auto-generated by napi-rs */
2
+ 'use strict'
3
+
4
+ const { existsSync, readFileSync } = require('fs')
5
+ const { join } = require('path')
6
+
7
+ const { platform, arch } = process
8
+
9
+ let nativeBinding = null
10
+ let localFileExisted = false
11
+ let loadError = null
12
+
13
+ function isMusl() {
14
+ if (!existsSync('/usr/bin/ldd')) return true
15
+ return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
16
+ }
17
+
18
+ switch (platform) {
19
+ case 'android':
20
+ switch (arch) {
21
+ case 'arm64': nativeBinding = require('@deepstrike/core-android-arm64'); break
22
+ case 'arm': nativeBinding = require('@deepstrike/core-android-arm-eabi'); break
23
+ default: throw new Error(`Unsupported architecture on Android: ${arch}`)
24
+ }
25
+ break
26
+ case 'win32':
27
+ switch (arch) {
28
+ case 'x64': nativeBinding = require('@deepstrike/core-win32-x64-msvc'); break
29
+ case 'ia32': nativeBinding = require('@deepstrike/core-win32-ia32-msvc'); break
30
+ case 'arm64': nativeBinding = require('@deepstrike/core-win32-arm64-msvc'); break
31
+ default: throw new Error(`Unsupported architecture on Windows: ${arch}`)
32
+ }
33
+ break
34
+ case 'darwin':
35
+ localFileExisted = existsSync(join(__dirname, 'deepstrike-core.darwin-universal.node'))
36
+ try {
37
+ if (localFileExisted) { nativeBinding = require('./deepstrike-core.darwin-universal.node'); break }
38
+ } catch {}
39
+ switch (arch) {
40
+ case 'x64': nativeBinding = require('@deepstrike/core-darwin-x64'); break
41
+ case 'arm64': nativeBinding = require('@deepstrike/core-darwin-arm64'); break
42
+ default: throw new Error(`Unsupported architecture on macOS: ${arch}`)
43
+ }
44
+ break
45
+ case 'freebsd':
46
+ if (arch !== 'x64') throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
47
+ nativeBinding = require('@deepstrike/core-freebsd-x64')
48
+ break
49
+ case 'linux':
50
+ switch (arch) {
51
+ case 'x64':
52
+ nativeBinding = isMusl()
53
+ ? require('@deepstrike/core-linux-x64-musl')
54
+ : require('@deepstrike/core-linux-x64-gnu')
55
+ break
56
+ case 'arm64':
57
+ nativeBinding = isMusl()
58
+ ? require('@deepstrike/core-linux-arm64-musl')
59
+ : require('@deepstrike/core-linux-arm64-gnu')
60
+ break
61
+ case 'arm':
62
+ nativeBinding = require('@deepstrike/core-linux-arm-gnueabihf')
63
+ break
64
+ default:
65
+ throw new Error(`Unsupported architecture on Linux: ${arch}`)
66
+ }
67
+ break
68
+ default:
69
+ throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
70
+ }
71
+
72
+ module.exports = nativeBinding
@@ -0,0 +1,3 @@
1
+ # `@deepstrike/core-darwin-arm64`
2
+
3
+ This is the **aarch64-apple-darwin** binary for `@deepstrike/core`
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@deepstrike/core-darwin-arm64",
3
+ "version": "0.1.0",
4
+ "cpu": [
5
+ "arm64"
6
+ ],
7
+ "main": "deepstrike-core.darwin-arm64.node",
8
+ "files": [
9
+ "deepstrike-core.darwin-arm64.node"
10
+ ],
11
+ "description": "DeepStrike kernel — pre-built native addon",
12
+ "license": "Apache-2.0 OR MIT",
13
+ "os": [
14
+ "darwin"
15
+ ]
16
+ }
@@ -0,0 +1,3 @@
1
+ # `@deepstrike/core-darwin-x64`
2
+
3
+ This is the **x86_64-apple-darwin** binary for `@deepstrike/core`
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@deepstrike/core-darwin-x64",
3
+ "version": "0.1.0",
4
+ "cpu": [
5
+ "x64"
6
+ ],
7
+ "main": "deepstrike-core.darwin-x64.node",
8
+ "files": [
9
+ "deepstrike-core.darwin-x64.node"
10
+ ],
11
+ "description": "DeepStrike kernel — pre-built native addon",
12
+ "license": "Apache-2.0 OR MIT",
13
+ "os": [
14
+ "darwin"
15
+ ]
16
+ }
@@ -0,0 +1,3 @@
1
+ # `@deepstrike/core-linux-arm64-gnu`
2
+
3
+ This is the **aarch64-unknown-linux-gnu** binary for `@deepstrike/core`
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@deepstrike/core-linux-arm64-gnu",
3
+ "version": "0.1.0",
4
+ "cpu": [
5
+ "arm64"
6
+ ],
7
+ "main": "deepstrike-core.linux-arm64-gnu.node",
8
+ "files": [
9
+ "deepstrike-core.linux-arm64-gnu.node"
10
+ ],
11
+ "description": "DeepStrike kernel — pre-built native addon",
12
+ "license": "Apache-2.0 OR MIT",
13
+ "os": [
14
+ "linux"
15
+ ],
16
+ "libc": [
17
+ "glibc"
18
+ ]
19
+ }
@@ -0,0 +1,3 @@
1
+ # `@deepstrike/core-linux-arm64-musl`
2
+
3
+ This is the **aarch64-unknown-linux-musl** binary for `@deepstrike/core`
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@deepstrike/core-linux-arm64-musl",
3
+ "version": "0.1.0",
4
+ "cpu": [
5
+ "arm64"
6
+ ],
7
+ "main": "deepstrike-core.linux-arm64-musl.node",
8
+ "files": [
9
+ "deepstrike-core.linux-arm64-musl.node"
10
+ ],
11
+ "description": "DeepStrike kernel — pre-built native addon",
12
+ "license": "Apache-2.0 OR MIT",
13
+ "os": [
14
+ "linux"
15
+ ],
16
+ "libc": [
17
+ "musl"
18
+ ]
19
+ }
@@ -0,0 +1,3 @@
1
+ # `@deepstrike/core-linux-x64-gnu`
2
+
3
+ This is the **x86_64-unknown-linux-gnu** binary for `@deepstrike/core`
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@deepstrike/core-linux-x64-gnu",
3
+ "version": "0.1.0",
4
+ "cpu": [
5
+ "x64"
6
+ ],
7
+ "main": "deepstrike-core.linux-x64-gnu.node",
8
+ "files": [
9
+ "deepstrike-core.linux-x64-gnu.node"
10
+ ],
11
+ "description": "DeepStrike kernel — pre-built native addon",
12
+ "license": "Apache-2.0 OR MIT",
13
+ "os": [
14
+ "linux"
15
+ ],
16
+ "libc": [
17
+ "glibc"
18
+ ]
19
+ }
@@ -0,0 +1,3 @@
1
+ # `@deepstrike/core-linux-x64-musl`
2
+
3
+ This is the **x86_64-unknown-linux-musl** binary for `@deepstrike/core`
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@deepstrike/core-linux-x64-musl",
3
+ "version": "0.1.0",
4
+ "cpu": [
5
+ "x64"
6
+ ],
7
+ "main": "deepstrike-core.linux-x64-musl.node",
8
+ "files": [
9
+ "deepstrike-core.linux-x64-musl.node"
10
+ ],
11
+ "description": "DeepStrike kernel — pre-built native addon",
12
+ "license": "Apache-2.0 OR MIT",
13
+ "os": [
14
+ "linux"
15
+ ],
16
+ "libc": [
17
+ "musl"
18
+ ]
19
+ }
@@ -0,0 +1,3 @@
1
+ # `@deepstrike/core-win32-x64-msvc`
2
+
3
+ This is the **x86_64-pc-windows-msvc** binary for `@deepstrike/core`
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@deepstrike/core-win32-x64-msvc",
3
+ "version": "0.1.0",
4
+ "cpu": [
5
+ "x64"
6
+ ],
7
+ "main": "deepstrike-core.win32-x64-msvc.node",
8
+ "files": [
9
+ "deepstrike-core.win32-x64-msvc.node"
10
+ ],
11
+ "description": "DeepStrike kernel — pre-built native addon",
12
+ "license": "Apache-2.0 OR MIT",
13
+ "os": [
14
+ "win32"
15
+ ]
16
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@deepstrike/core",
3
+ "version": "0.1.0",
4
+ "description": "DeepStrike kernel — pre-built native addon",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "license": "Apache-2.0 OR MIT",
8
+ "napi": {
9
+ "binaryName": "deepstrike-core",
10
+ "targets": [
11
+ "x86_64-apple-darwin",
12
+ "aarch64-apple-darwin",
13
+ "x86_64-unknown-linux-gnu",
14
+ "aarch64-unknown-linux-gnu",
15
+ "x86_64-unknown-linux-musl",
16
+ "aarch64-unknown-linux-musl",
17
+ "x86_64-pc-windows-msvc"
18
+ ]
19
+ },
20
+ "optionalDependencies": {
21
+ "@deepstrike/core-linux-x64-gnu": "0.1.0",
22
+ "@deepstrike/core-linux-arm64-gnu": "0.1.0",
23
+ "@deepstrike/core-linux-x64-musl": "0.1.0",
24
+ "@deepstrike/core-linux-arm64-musl": "0.1.0",
25
+ "@deepstrike/core-darwin-x64": "0.1.0",
26
+ "@deepstrike/core-darwin-arm64": "0.1.0",
27
+ "@deepstrike/core-win32-x64-msvc": "0.1.0"
28
+ }
29
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,1030 @@
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::pipeline::GovernancePipeline as RustGovernancePipeline;
47
+ use deepstrike_core::harness::eval_pipeline::{
48
+ EvalAction as RustEvalAction, EvalEvent as RustEvalEvent, EvalPolicy as RustEvalPolicy,
49
+ EvalPipeline as RustEvalPipeline,
50
+ };
51
+ use deepstrike_core::memory::curator::CurationResult as RustCurationResult;
52
+ use deepstrike_core::memory::durable::SessionData as RustSessionData;
53
+ use deepstrike_core::memory::idle_pipeline::{
54
+ IdleAction as RustIdleAction, IdleEvent as RustIdleEvent, IdlePolicy as RustIdlePolicy,
55
+ IdlePipeline as RustIdlePipeline,
56
+ };
57
+ use deepstrike_core::memory::semantic::MemoryEntry as RustMemoryEntry;
58
+ use deepstrike_core::scheduler::policy::LoopPolicy as RustLoopPolicy;
59
+ use deepstrike_core::scheduler::state_machine::{
60
+ LoopAction as RustLoopAction, LoopEvent as RustLoopEvent,
61
+ LoopObservation as RustLoopObservation, LoopStateMachine as RustLoopStateMachine,
62
+ };
63
+ use deepstrike_core::signals::router::SignalRouter as RustSignalRouter;
64
+ use deepstrike_core::types::policy::SignalDisposition as RustSignalDisposition;
65
+ use deepstrike_core::types::signal::{
66
+ RuntimeSignal as RustRuntimeSignal, SignalSource as RustSignalSource,
67
+ SignalType as RustSignalType, Urgency as RustUrgency,
68
+ };
69
+ use deepstrike_core::types::message::{
70
+ Content, ContentPart, Message as RustMessage, Role, ToolCall as RustToolCall,
71
+ ToolResult as RustToolResult, ToolSchema as RustToolSchema,
72
+ };
73
+ use deepstrike_core::types::result::LoopResult as RustLoopResult;
74
+ use deepstrike_core::types::skill::SkillMetadata as RustSkillMetadata;
75
+ use deepstrike_core::types::task::RuntimeTask as RustRuntimeTask;
76
+
77
+ // ────────────────────────────────────── POD types (plain JS objects) ──────────────────────────────────────
78
+
79
+ #[napi(object)]
80
+ #[derive(Clone)]
81
+ pub struct Message {
82
+ pub role: String,
83
+ pub content: String,
84
+ pub token_count: Option<u32>,
85
+ pub tool_calls: Vec<ToolCall>,
86
+ }
87
+
88
+ #[napi(object)]
89
+ #[derive(Clone)]
90
+ pub struct ToolCall {
91
+ pub id: String,
92
+ pub name: String,
93
+ /// JSON-encoded arguments. JS: `JSON.stringify(args)`.
94
+ pub arguments: String,
95
+ }
96
+
97
+ #[napi(object)]
98
+ #[derive(Clone)]
99
+ pub struct ToolResult {
100
+ pub call_id: String,
101
+ pub output: String,
102
+ pub is_error: bool,
103
+ pub token_count: Option<u32>,
104
+ }
105
+
106
+ #[napi(object)]
107
+ #[derive(Clone)]
108
+ pub struct ToolSchema {
109
+ pub name: String,
110
+ pub description: String,
111
+ /// JSON-encoded JSON Schema. JS: `JSON.stringify(schema)`.
112
+ pub parameters: String,
113
+ }
114
+
115
+ #[napi(object)]
116
+ #[derive(Clone)]
117
+ pub struct RuntimeTask {
118
+ pub goal: String,
119
+ pub criteria: Option<Vec<String>>,
120
+ }
121
+
122
+ #[napi(object)]
123
+ #[derive(Clone)]
124
+ pub struct LoopPolicy {
125
+ pub max_tokens: u32,
126
+ pub max_turns: Option<u32>,
127
+ pub max_total_tokens: Option<BigInt>,
128
+ pub timeout_ms: Option<BigInt>,
129
+ }
130
+
131
+ #[napi(object)]
132
+ #[derive(Clone)]
133
+ pub struct LoopResult {
134
+ pub termination: String,
135
+ pub final_message: Option<Message>,
136
+ pub turns_used: u32,
137
+ pub total_tokens_used: BigInt,
138
+ }
139
+
140
+ // ────────────────────────────────────── Skill types ──────────────────────────────────────
141
+
142
+ // ────────────────────────────────────── Signal types ──────────────────────────────────────
143
+
144
+ /// Unified RuntimeSignal exposed to Node.js — mirrors the kernel type.
145
+ #[napi(object)]
146
+ #[derive(Clone)]
147
+ pub struct RuntimeSignal {
148
+ pub id: String,
149
+ /// "cron" | "gateway" | "heartbeat" | "custom"
150
+ pub source: String,
151
+ /// "event" | "job" | "alert"
152
+ pub signal_type: String,
153
+ /// "low" | "normal" | "high" | "critical"
154
+ pub urgency: String,
155
+ pub summary: String,
156
+ /// JSON-encoded payload.
157
+ pub payload: String,
158
+ pub dedupe_key: Option<String>,
159
+ pub timestamp_ms: f64,
160
+ }
161
+
162
+ fn runtime_signal_to_rust(s: RuntimeSignal) -> Result<RustRuntimeSignal> {
163
+ let source = match s.source.as_str() {
164
+ "cron" => RustSignalSource::Cron,
165
+ "gateway" => RustSignalSource::Gateway,
166
+ "heartbeat" => RustSignalSource::Heartbeat,
167
+ _ => RustSignalSource::Custom,
168
+ };
169
+ let signal_type = match s.signal_type.as_str() {
170
+ "job" => RustSignalType::Job,
171
+ "alert" => RustSignalType::Alert,
172
+ _ => RustSignalType::Event,
173
+ };
174
+ let urgency = match s.urgency.as_str() {
175
+ "critical" => RustUrgency::Critical,
176
+ "high" => RustUrgency::High,
177
+ "low" => RustUrgency::Low,
178
+ _ => RustUrgency::Normal,
179
+ };
180
+ let payload: serde_json::Value = serde_json::from_str(&s.payload)
181
+ .unwrap_or(serde_json::Value::Null);
182
+ let mut sig = RustRuntimeSignal::new(source, signal_type, urgency, s.summary.as_str())
183
+ .with_payload(payload)
184
+ .with_timestamp(s.timestamp_ms as u64);
185
+ if let Some(key) = s.dedupe_key {
186
+ sig = sig.with_dedupe(key.as_str());
187
+ }
188
+ Ok(sig)
189
+ }
190
+
191
+ fn runtime_signal_from_rust(s: &RustRuntimeSignal) -> RuntimeSignal {
192
+ let source = match s.source {
193
+ RustSignalSource::Cron => "cron",
194
+ RustSignalSource::Gateway => "gateway",
195
+ RustSignalSource::Heartbeat => "heartbeat",
196
+ RustSignalSource::Custom => "custom",
197
+ };
198
+ let signal_type = match s.signal_type {
199
+ RustSignalType::Event => "event",
200
+ RustSignalType::Job => "job",
201
+ RustSignalType::Alert => "alert",
202
+ };
203
+ let urgency = match s.urgency {
204
+ RustUrgency::Critical => "critical",
205
+ RustUrgency::High => "high",
206
+ RustUrgency::Normal => "normal",
207
+ RustUrgency::Low => "low",
208
+ };
209
+ RuntimeSignal {
210
+ id: s.id.to_string(),
211
+ source: source.into(),
212
+ signal_type: signal_type.into(),
213
+ urgency: urgency.into(),
214
+ summary: s.summary.to_string(),
215
+ payload: serde_json::to_string(&s.payload).unwrap_or_else(|_| "null".into()),
216
+ dedupe_key: s.dedupe_key.as_ref().map(|k| k.to_string()),
217
+ timestamp_ms: s.timestamp_ms as f64,
218
+ }
219
+ }
220
+
221
+ fn disposition_to_str(d: RustSignalDisposition) -> &'static str {
222
+ match d {
223
+ RustSignalDisposition::Ignore => "ignore",
224
+ RustSignalDisposition::Observe => "observe",
225
+ RustSignalDisposition::Queue => "queue",
226
+ RustSignalDisposition::Run { .. } => "run",
227
+ RustSignalDisposition::Interrupt => "interrupt",
228
+ RustSignalDisposition::InterruptNow => "interrupt_now",
229
+ RustSignalDisposition::Dropped => "dropped",
230
+ }
231
+ }
232
+
233
+ #[napi(object)]
234
+ #[derive(Clone)]
235
+ pub struct SkillMetadata {
236
+ pub name: String,
237
+ pub description: String,
238
+ pub when_to_use: Option<String>,
239
+ pub allowed_tools: Option<Vec<String>>,
240
+ pub effort: Option<u8>,
241
+ pub estimated_tokens: u32,
242
+ }
243
+
244
+ // ────────────────────────────── Tagged unions: LoopAction / LoopObservation ──────────────────────────────
245
+
246
+ /// Discriminated union. Inspect `kind`:
247
+ /// - `"call_llm"` → `messages`, `tools` (includes `skill` meta-tool when skills are registered)
248
+ /// - `"execute_tools"` → `calls`
249
+ /// - `"done"` → `result`
250
+ #[napi(object)]
251
+ #[derive(Clone)]
252
+ pub struct LoopAction {
253
+ pub kind: String,
254
+ pub messages: Option<Vec<Message>>,
255
+ pub tools: Option<Vec<ToolSchema>>,
256
+ pub calls: Option<Vec<ToolCall>>,
257
+ pub result: Option<LoopResult>,
258
+ }
259
+
260
+ /// Discriminated union for observations:
261
+ /// - `"compressed"` → `action`, `rho_after`
262
+ #[napi(object)]
263
+ #[derive(Clone)]
264
+ pub struct LoopObservation {
265
+ pub kind: String,
266
+ pub action: Option<String>,
267
+ pub rho_after: Option<f64>,
268
+ }
269
+
270
+ // ────────────────────────────────── conversion helpers ──────────────────────────────────
271
+
272
+ fn role_str_to_rust(role: &str) -> Result<Role> {
273
+ match role {
274
+ "system" => Ok(Role::System),
275
+ "user" => Ok(Role::User),
276
+ "assistant" => Ok(Role::Assistant),
277
+ "tool" => Ok(Role::Tool),
278
+ other => Err(Error::new(Status::InvalidArg, format!("invalid role: {other}"))),
279
+ }
280
+ }
281
+
282
+ fn role_to_str(role: Role) -> &'static str {
283
+ match role {
284
+ Role::System => "system",
285
+ Role::User => "user",
286
+ Role::Assistant => "assistant",
287
+ Role::Tool => "tool",
288
+ }
289
+ }
290
+
291
+ fn message_to_rust(m: Message) -> Result<RustMessage> {
292
+ let role = role_str_to_rust(&m.role)?;
293
+ let tool_calls: Vec<RustToolCall> = m
294
+ .tool_calls
295
+ .into_iter()
296
+ .map(tool_call_to_rust)
297
+ .collect::<Result<_>>()?;
298
+ Ok(RustMessage {
299
+ role,
300
+ content: Content::Text(m.content),
301
+ tool_calls,
302
+ token_count: m.token_count,
303
+ })
304
+ }
305
+
306
+ fn message_from_rust(m: &RustMessage) -> Message {
307
+ let content = match &m.content {
308
+ Content::Text(s) => s.clone(),
309
+ Content::Parts(parts) => parts
310
+ .iter()
311
+ .map(|p| match p {
312
+ ContentPart::Text { text } => text.clone(),
313
+ ContentPart::Image { url } => format!("[image: {url}]"),
314
+ ContentPart::ToolResult { call_id, output, .. } => {
315
+ format!("[tool_result {call_id}]: {output}")
316
+ }
317
+ })
318
+ .collect::<Vec<_>>()
319
+ .join("\n"),
320
+ };
321
+ Message {
322
+ role: role_to_str(m.role).to_string(),
323
+ content,
324
+ token_count: m.token_count,
325
+ tool_calls: m.tool_calls.iter().map(tool_call_from_rust).collect(),
326
+ }
327
+ }
328
+
329
+ fn tool_call_to_rust(c: ToolCall) -> Result<RustToolCall> {
330
+ let args: serde_json::Value = serde_json::from_str(&c.arguments)
331
+ .map_err(|e| Error::new(Status::InvalidArg, format!("invalid JSON arguments: {e}")))?;
332
+ Ok(RustToolCall {
333
+ id: CompactString::new(&c.id),
334
+ name: CompactString::new(&c.name),
335
+ arguments: args,
336
+ })
337
+ }
338
+
339
+ fn tool_call_from_rust(c: &RustToolCall) -> ToolCall {
340
+ ToolCall {
341
+ id: c.id.to_string(),
342
+ name: c.name.to_string(),
343
+ arguments: serde_json::to_string(&c.arguments).unwrap_or_else(|_| "null".into()),
344
+ }
345
+ }
346
+
347
+ fn tool_result_to_rust(r: ToolResult) -> RustToolResult {
348
+ RustToolResult {
349
+ call_id: CompactString::new(&r.call_id),
350
+ output: Content::Text(r.output),
351
+ is_error: r.is_error,
352
+ token_count: r.token_count,
353
+ }
354
+ }
355
+
356
+ fn tool_schema_to_rust(t: ToolSchema) -> Result<RustToolSchema> {
357
+ let params: serde_json::Value = serde_json::from_str(&t.parameters)
358
+ .map_err(|e| Error::new(Status::InvalidArg, format!("invalid JSON parameters: {e}")))?;
359
+ Ok(RustToolSchema {
360
+ name: CompactString::new(&t.name),
361
+ description: t.description,
362
+ parameters: params,
363
+ })
364
+ }
365
+
366
+ fn tool_schema_from_rust(t: &RustToolSchema) -> ToolSchema {
367
+ ToolSchema {
368
+ name: t.name.to_string(),
369
+ description: t.description.clone(),
370
+ parameters: serde_json::to_string(&t.parameters).unwrap_or_else(|_| "null".into()),
371
+ }
372
+ }
373
+
374
+ fn skill_metadata_to_rust(s: SkillMetadata) -> RustSkillMetadata {
375
+ RustSkillMetadata {
376
+ name: CompactString::new(&s.name),
377
+ description: s.description,
378
+ when_to_use: s.when_to_use,
379
+ allowed_tools: s
380
+ .allowed_tools
381
+ .unwrap_or_default()
382
+ .iter()
383
+ .map(CompactString::new)
384
+ .collect(),
385
+ effort: s.effort,
386
+ estimated_tokens: s.estimated_tokens,
387
+ }
388
+ }
389
+
390
+ fn task_to_rust(t: RuntimeTask) -> RustRuntimeTask {
391
+ RustRuntimeTask {
392
+ goal: t.goal,
393
+ criteria: t.criteria.unwrap_or_default(),
394
+ metadata: serde_json::Value::Null,
395
+ }
396
+ }
397
+
398
+ fn policy_to_rust(p: LoopPolicy) -> RustLoopPolicy {
399
+ RustLoopPolicy {
400
+ max_tokens: p.max_tokens,
401
+ max_turns: p.max_turns.unwrap_or(25),
402
+ max_total_tokens: p
403
+ .max_total_tokens
404
+ .map(|b| b.get_u64().1)
405
+ .unwrap_or(1_000_000),
406
+ timeout_ms: p.timeout_ms.map(|b| b.get_u64().1),
407
+ }
408
+ }
409
+
410
+ fn pressure_action_str(a: PressureAction) -> &'static str {
411
+ match a {
412
+ PressureAction::None => "none",
413
+ PressureAction::SnipCompact => "snip_compact",
414
+ PressureAction::MicroCompact => "micro_compact",
415
+ PressureAction::ContextCollapse => "context_collapse",
416
+ PressureAction::AutoCompact => "auto_compact",
417
+ }
418
+ }
419
+
420
+ fn loop_result_from_rust(r: &RustLoopResult) -> LoopResult {
421
+ let termination = match r.termination {
422
+ deepstrike_core::types::result::TerminationReason::Completed => "completed",
423
+ deepstrike_core::types::result::TerminationReason::MaxTurns => "max_turns",
424
+ deepstrike_core::types::result::TerminationReason::TokenBudget => "token_budget",
425
+ deepstrike_core::types::result::TerminationReason::Timeout => "timeout",
426
+ deepstrike_core::types::result::TerminationReason::UserAbort => "user_abort",
427
+ deepstrike_core::types::result::TerminationReason::Error => "error",
428
+ };
429
+ LoopResult {
430
+ termination: termination.to_string(),
431
+ final_message: r.final_message.as_ref().map(message_from_rust),
432
+ turns_used: r.turns_used,
433
+ total_tokens_used: BigInt::from(r.total_tokens_used),
434
+ }
435
+ }
436
+
437
+ fn loop_action_from_rust(a: RustLoopAction) -> LoopAction {
438
+ match a {
439
+ RustLoopAction::CallLLM { messages, tools } => LoopAction {
440
+ kind: "call_llm".into(),
441
+ messages: Some(messages.iter().map(message_from_rust).collect()),
442
+ tools: Some(tools.iter().map(tool_schema_from_rust).collect()),
443
+ calls: None,
444
+ result: None,
445
+ },
446
+ RustLoopAction::ExecuteTools { calls } => LoopAction {
447
+ kind: "execute_tools".into(),
448
+ messages: None,
449
+ tools: None,
450
+ calls: Some(calls.iter().map(tool_call_from_rust).collect()),
451
+ result: None,
452
+ },
453
+ RustLoopAction::Done { result } => LoopAction {
454
+ kind: "done".into(),
455
+ messages: None,
456
+ tools: None,
457
+ calls: None,
458
+ result: Some(loop_result_from_rust(&result)),
459
+ },
460
+ }
461
+ }
462
+
463
+ fn observation_from_rust(o: RustLoopObservation) -> LoopObservation {
464
+ match o {
465
+ RustLoopObservation::Compressed { action, rho_after } => LoopObservation {
466
+ kind: "compressed".into(),
467
+ action: Some(pressure_action_str(action).into()),
468
+ rho_after: Some(rho_after),
469
+ },
470
+ }
471
+ }
472
+
473
+ // ─────────────────────────────────────────── ContextEngine ───────────────────────────────────────────
474
+
475
+ #[napi]
476
+ pub struct ContextEngine {
477
+ inner: ContextManager,
478
+ }
479
+
480
+ #[napi]
481
+ impl ContextEngine {
482
+ #[napi(constructor)]
483
+ pub fn new(max_tokens: u32) -> Self {
484
+ Self { inner: ContextManager::new(max_tokens) }
485
+ }
486
+
487
+ #[napi]
488
+ pub fn add_system_message(&mut self, content: String, tokens: u32) {
489
+ self.inner
490
+ .partitions
491
+ .system
492
+ .push(RustMessage::system(content), tokens);
493
+ }
494
+
495
+ #[napi]
496
+ pub fn add_user_message(&mut self, content: String, tokens: u32) {
497
+ self.inner.push_history(RustMessage::user(content), tokens);
498
+ }
499
+
500
+ #[napi]
501
+ pub fn add_assistant_message(&mut self, content: String, tokens: u32) {
502
+ self.inner
503
+ .push_history(RustMessage::assistant(content), tokens);
504
+ }
505
+
506
+ #[napi]
507
+ pub fn pressure(&self) -> f64 {
508
+ self.inner.rho()
509
+ }
510
+
511
+ #[napi]
512
+ pub fn total_tokens(&self) -> u32 {
513
+ self.inner.partitions.total_tokens()
514
+ }
515
+
516
+ /// Run compression at the level the current pressure recommends.
517
+ /// Returns tokens saved.
518
+ #[napi]
519
+ pub fn compress(&mut self) -> u32 {
520
+ let action = self.inner.should_compress();
521
+ if action == PressureAction::None {
522
+ return 0;
523
+ }
524
+ let before = self.inner.partitions.total_tokens();
525
+ self.inner.compress(action);
526
+ let after = self.inner.partitions.total_tokens();
527
+ before.saturating_sub(after)
528
+ }
529
+
530
+ #[napi]
531
+ pub fn render(&self) -> Vec<Message> {
532
+ self.inner.render().iter().map(message_from_rust).collect()
533
+ }
534
+
535
+ /// Replace the available-skills set with frontmatter-only metadata.
536
+ /// The kernel will auto-inject the `skill` meta-tool into every `CallLLM` action.
537
+ #[napi]
538
+ pub fn set_available_skills(&mut self, skills: Vec<SkillMetadata>) {
539
+ let rust_skills = skills.into_iter().map(skill_metadata_to_rust).collect();
540
+ self.inner.set_available_skills(rust_skills);
541
+ }
542
+ }
543
+
544
+ // ─────────────────────────────────────────── LoopStateMachine ───────────────────────────────────────────
545
+
546
+ #[napi]
547
+ pub struct LoopStateMachine {
548
+ inner: RustLoopStateMachine,
549
+ }
550
+
551
+ #[napi]
552
+ impl LoopStateMachine {
553
+ #[napi(constructor)]
554
+ pub fn new(policy: LoopPolicy) -> Self {
555
+ Self { inner: RustLoopStateMachine::new(policy_to_rust(policy)) }
556
+ }
557
+
558
+ /// Convenience: register skills directly on the state machine without
559
+ /// reaching into the inner ContextEngine.
560
+ #[napi]
561
+ pub fn set_available_skills(&mut self, skills: Vec<SkillMetadata>) {
562
+ let rust_skills = skills.into_iter().map(skill_metadata_to_rust).collect();
563
+ self.inner.ctx.set_available_skills(rust_skills);
564
+ }
565
+
566
+ /// Enable the `memory` meta-tool. Call with `true` when a DreamStore and agentId
567
+ /// are configured — the SDK layer intercepts `memory` tool calls and runs the search.
568
+ #[napi]
569
+ pub fn set_memory_enabled(&mut self, enabled: bool) {
570
+ self.inner.ctx.set_memory_enabled(enabled);
571
+ }
572
+
573
+ /// Enable the `knowledge` meta-tool. Call with `true` when a KnowledgeSource
574
+ /// is configured — the SDK layer intercepts `knowledge` tool calls and runs retrieval.
575
+ #[napi]
576
+ pub fn set_knowledge_enabled(&mut self, enabled: bool) {
577
+ self.inner.ctx.set_knowledge_enabled(enabled);
578
+ }
579
+
580
+ #[napi]
581
+ pub fn set_tools(&mut self, tools: Vec<ToolSchema>) -> Result<()> {
582
+ let rust_tools: Vec<RustToolSchema> = tools
583
+ .into_iter()
584
+ .map(tool_schema_to_rust)
585
+ .collect::<Result<_>>()?;
586
+ self.inner.tools = rust_tools;
587
+ Ok(())
588
+ }
589
+
590
+ #[napi]
591
+ pub fn start(&mut self, task: RuntimeTask) -> LoopAction {
592
+ loop_action_from_rust(self.inner.start(task_to_rust(task)))
593
+ }
594
+
595
+ #[napi]
596
+ pub fn feed_llm_response(&mut self, message: Message) -> Result<LoopAction> {
597
+ let msg = message_to_rust(message)?;
598
+ Ok(loop_action_from_rust(
599
+ self.inner.feed(RustLoopEvent::LLMResponse { message: msg }),
600
+ ))
601
+ }
602
+
603
+ #[napi]
604
+ pub fn feed_tool_results(&mut self, results: Vec<ToolResult>) -> LoopAction {
605
+ let results: Vec<RustToolResult> = results.into_iter().map(tool_result_to_rust).collect();
606
+ loop_action_from_rust(self.inner.feed(RustLoopEvent::ToolResults { results }))
607
+ }
608
+
609
+ #[napi]
610
+ pub fn feed_timeout(&mut self) -> LoopAction {
611
+ loop_action_from_rust(self.inner.feed(RustLoopEvent::Timeout))
612
+ }
613
+
614
+ #[napi]
615
+ pub fn is_terminal(&self) -> bool {
616
+ self.inner.is_terminal()
617
+ }
618
+
619
+ #[napi]
620
+ pub fn turn(&self) -> u32 {
621
+ self.inner.turn
622
+ }
623
+
624
+ #[napi]
625
+ pub fn pressure(&self) -> f64 {
626
+ self.inner.ctx.rho()
627
+ }
628
+
629
+ /// Drain observations emitted during the most recent feed call.
630
+ #[napi]
631
+ pub fn take_observations(&mut self) -> Vec<LoopObservation> {
632
+ self.inner
633
+ .take_observations()
634
+ .into_iter()
635
+ .map(observation_from_rust)
636
+ .collect()
637
+ }
638
+
639
+ #[napi]
640
+ pub fn render(&self) -> Vec<Message> {
641
+ self.inner.ctx.render().iter().map(message_from_rust).collect()
642
+ }
643
+ }
644
+
645
+ // ─────────────────────────────────────────── SignalRouter ───────────────────────────────────────────
646
+
647
+ #[napi]
648
+ pub struct SignalRouter {
649
+ inner: RustSignalRouter,
650
+ }
651
+
652
+ #[napi]
653
+ impl SignalRouter {
654
+ #[napi(constructor)]
655
+ pub fn new(max_queue_size: u32) -> Self {
656
+ Self { inner: RustSignalRouter::new(max_queue_size as usize) }
657
+ }
658
+
659
+ /// Ingest a signal. Returns the disposition string:
660
+ /// "ignore" | "observe" | "queue" | "run" | "interrupt" | "interrupt_now" | "dropped"
661
+ #[napi]
662
+ pub fn ingest(&mut self, signal: RuntimeSignal, is_running: bool) -> Result<String> {
663
+ let rust_sig = runtime_signal_to_rust(signal)?;
664
+ Ok(disposition_to_str(self.inner.ingest(rust_sig, is_running)).into())
665
+ }
666
+
667
+ /// Pull the next queued signal (highest priority first).
668
+ #[napi]
669
+ pub fn next(&mut self) -> Option<RuntimeSignal> {
670
+ self.inner.next().as_ref().map(runtime_signal_from_rust)
671
+ }
672
+
673
+ #[napi]
674
+ pub fn depth(&self) -> u32 {
675
+ self.inner.depth() as u32
676
+ }
677
+
678
+ #[napi]
679
+ pub fn clear_dedup(&mut self) {
680
+ self.inner.clear_dedup();
681
+ }
682
+ }
683
+
684
+ // ─────────────────────────────────────────── Governance ───────────────────────────────────────────
685
+
686
+ #[napi]
687
+ pub struct Governance {
688
+ inner: RustGovernancePipeline,
689
+ }
690
+
691
+ #[napi]
692
+ impl Governance {
693
+ #[napi(constructor)]
694
+ pub fn new() -> Self {
695
+ Self { inner: RustGovernancePipeline::default() }
696
+ }
697
+
698
+ #[napi]
699
+ pub fn block_tool(&mut self, name: String) {
700
+ self.inner.veto.block_tool(name);
701
+ }
702
+
703
+ #[napi]
704
+ pub fn set_time(&mut self, now_ms: BigInt) {
705
+ self.inner.set_time(now_ms.get_u64().1);
706
+ }
707
+ }
708
+
709
+ // ──────────────────────────────── Dream / idle-pipeline POD types ────────────────────────────────
710
+
711
+ /// A single session of agent messages, used as input to `IdlePipeline.feedTrigger`.
712
+ #[napi(object)]
713
+ #[derive(Clone)]
714
+ pub struct SessionData {
715
+ pub session_id: String,
716
+ pub agent_id: String,
717
+ /// Messages from this session.
718
+ pub messages: Vec<Message>,
719
+ /// JSON-encoded metadata blob.
720
+ pub metadata: String,
721
+ /// Unix ms timestamp.
722
+ pub created_at_ms: f64,
723
+ /// Unix ms timestamp.
724
+ pub updated_at_ms: f64,
725
+ }
726
+
727
+ /// A long-term memory entry as stored by the agent.
728
+ #[napi(object)]
729
+ #[derive(Clone)]
730
+ pub struct MemoryEntry {
731
+ pub text: String,
732
+ pub score: f64,
733
+ /// JSON-encoded metadata blob.
734
+ pub metadata: String,
735
+ }
736
+
737
+ #[napi(object)]
738
+ #[derive(Clone)]
739
+ pub struct CurationStats {
740
+ pub insights_processed: u32,
741
+ pub duplicates_removed: u32,
742
+ pub conflicts_resolved: u32,
743
+ pub entries_added: u32,
744
+ }
745
+
746
+ /// The delta the `DreamStore.commit` must apply: add `toAdd`, remove `toRemoveIndices`.
747
+ #[napi(object)]
748
+ #[derive(Clone)]
749
+ pub struct CurationResult {
750
+ pub to_add: Vec<MemoryEntry>,
751
+ /// Indices into the `existingMemories` slice passed to `feedTrigger`.
752
+ pub to_remove_indices: Vec<u32>,
753
+ pub stats: CurationStats,
754
+ }
755
+
756
+ #[napi(object)]
757
+ #[derive(Clone)]
758
+ pub struct IdleRunResult {
759
+ pub sessions_processed: u32,
760
+ pub insights_extracted: u32,
761
+ }
762
+
763
+ /// Discriminated union returned by `IdlePipeline` methods. Inspect `kind`:
764
+ /// - `"synthesize_insights"` → `messages` (SDK must call LLM, then `feedSynthesisResult`)
765
+ /// - `"commit_memories"` → `agentId`, `curationResult`, `runResult`
766
+ /// - `"noop"` | `"aborted"`
767
+ #[napi(object)]
768
+ #[derive(Clone)]
769
+ pub struct IdlePipelineAction {
770
+ pub kind: String,
771
+ pub messages: Option<Vec<Message>>,
772
+ pub agent_id: Option<String>,
773
+ pub curation_result: Option<CurationResult>,
774
+ pub run_result: Option<IdleRunResult>,
775
+ }
776
+
777
+ // ─────────────────────── Dream conversion helpers ───────────────────────
778
+
779
+ fn session_data_to_rust(s: SessionData) -> Result<RustSessionData> {
780
+ let messages: Vec<RustMessage> =
781
+ s.messages.into_iter().map(message_to_rust).collect::<Result<_>>()?;
782
+ let metadata: serde_json::Value =
783
+ serde_json::from_str(&s.metadata).unwrap_or(serde_json::Value::Null);
784
+ Ok(RustSessionData {
785
+ session_id: s.session_id,
786
+ agent_id: s.agent_id,
787
+ messages,
788
+ metadata,
789
+ created_at_ms: s.created_at_ms as u64,
790
+ updated_at_ms: s.updated_at_ms as u64,
791
+ })
792
+ }
793
+
794
+ fn memory_entry_to_rust(e: MemoryEntry) -> RustMemoryEntry {
795
+ let metadata: serde_json::Value =
796
+ serde_json::from_str(&e.metadata).unwrap_or(serde_json::Value::Null);
797
+ RustMemoryEntry { text: e.text, score: e.score, metadata }
798
+ }
799
+
800
+ fn memory_entry_from_rust(e: &RustMemoryEntry) -> MemoryEntry {
801
+ MemoryEntry {
802
+ text: e.text.clone(),
803
+ score: e.score,
804
+ metadata: serde_json::to_string(&e.metadata).unwrap_or_else(|_| "null".into()),
805
+ }
806
+ }
807
+
808
+ fn curation_result_from_rust(r: RustCurationResult) -> CurationResult {
809
+ CurationResult {
810
+ to_add: r.to_add.iter().map(memory_entry_from_rust).collect(),
811
+ to_remove_indices: r.to_remove_indices.iter().map(|&i| i as u32).collect(),
812
+ stats: CurationStats {
813
+ insights_processed: r.stats.insights_processed as u32,
814
+ duplicates_removed: r.stats.duplicates_removed as u32,
815
+ conflicts_resolved: r.stats.conflicts_resolved as u32,
816
+ entries_added: r.stats.entries_added as u32,
817
+ },
818
+ }
819
+ }
820
+
821
+ fn idle_pipeline_action_from_rust(a: RustIdleAction) -> IdlePipelineAction {
822
+ match a {
823
+ RustIdleAction::SynthesizeInsights { messages } => IdlePipelineAction {
824
+ kind: "synthesize_insights".into(),
825
+ messages: Some(messages.iter().map(message_from_rust).collect()),
826
+ agent_id: None,
827
+ curation_result: None,
828
+ run_result: None,
829
+ },
830
+ RustIdleAction::CommitMemories { agent_id, result, run_result } => IdlePipelineAction {
831
+ kind: "commit_memories".into(),
832
+ messages: None,
833
+ agent_id: Some(agent_id),
834
+ curation_result: Some(curation_result_from_rust(result)),
835
+ run_result: Some(IdleRunResult {
836
+ sessions_processed: run_result.sessions_processed as u32,
837
+ insights_extracted: run_result.insights_extracted as u32,
838
+ }),
839
+ },
840
+ RustIdleAction::Noop => IdlePipelineAction {
841
+ kind: "noop".into(),
842
+ messages: None,
843
+ agent_id: None,
844
+ curation_result: None,
845
+ run_result: None,
846
+ },
847
+ RustIdleAction::Aborted => IdlePipelineAction {
848
+ kind: "aborted".into(),
849
+ messages: None,
850
+ agent_id: None,
851
+ curation_result: None,
852
+ run_result: None,
853
+ },
854
+ }
855
+ }
856
+
857
+ // ─────────────────────────────────────────── EvalPipeline ────────────────────────────────────────
858
+
859
+ #[napi(object)]
860
+ #[derive(Clone)]
861
+ pub struct EvalPipelineOptions {
862
+ pub extract_skill_on_pass: Option<bool>,
863
+ }
864
+
865
+ #[napi(object)]
866
+ #[derive(Clone)]
867
+ pub struct SkillCandidate {
868
+ pub name: String,
869
+ pub description: String,
870
+ pub when_to_use: Option<String>,
871
+ pub content: String,
872
+ }
873
+
874
+ /// Discriminated union returned by `EvalPipeline` methods. Inspect `kind`:
875
+ /// - `"evaluate"` → `messages` (SDK must call evaluator LLM, then `feedEvalResult`)
876
+ /// - `"done"` → `result` with `passed`, `feedback`, optional `skillCandidate`
877
+ #[napi(object)]
878
+ #[derive(Clone)]
879
+ pub struct EvalPipelineAction {
880
+ pub kind: String,
881
+ pub messages: Option<Vec<Message>>,
882
+ pub passed: Option<bool>,
883
+ pub feedback: Option<String>,
884
+ pub skill_candidate: Option<SkillCandidate>,
885
+ }
886
+
887
+ /// Kernel state machine for the evaluation cycle.
888
+ ///
889
+ /// Drive it like this:
890
+ /// 1. `feedOutcome(goal, criteria, result, attempt)` → `"evaluate"` action
891
+ /// 2. Call evaluator LLM with `action.messages`, collect the text response
892
+ /// 3. `feedEvalResult(text)` → `"done"` action
893
+ /// 4. Read `action.passed` / `action.feedback` / `action.skillCandidate`
894
+ /// 5. Call `reset()` before the next attempt
895
+ #[napi]
896
+ pub struct EvalPipeline {
897
+ inner: RustEvalPipeline,
898
+ }
899
+
900
+ #[napi]
901
+ impl EvalPipeline {
902
+ #[napi(constructor)]
903
+ pub fn new(options: Option<EvalPipelineOptions>) -> Self {
904
+ let policy = RustEvalPolicy {
905
+ extract_skill_on_pass: options
906
+ .and_then(|o| o.extract_skill_on_pass)
907
+ .unwrap_or(true),
908
+ };
909
+ Self { inner: RustEvalPipeline::new(policy) }
910
+ }
911
+
912
+ /// Phase 1 — provide the goal, criteria, agent output, and attempt number.
913
+ /// Returns an `"evaluate"` action with messages to send to the evaluator LLM.
914
+ #[napi]
915
+ pub fn feed_outcome(
916
+ &mut self,
917
+ goal: String,
918
+ criteria: Vec<String>,
919
+ result: String,
920
+ attempt: u32,
921
+ ) -> EvalPipelineAction {
922
+ match self.inner.feed(RustEvalEvent::Outcome { goal, criteria, result, attempt }) {
923
+ RustEvalAction::Evaluate { messages } => EvalPipelineAction {
924
+ kind: "evaluate".into(),
925
+ messages: Some(messages.iter().map(message_from_rust).collect()),
926
+ passed: None,
927
+ feedback: None,
928
+ skill_candidate: None,
929
+ },
930
+ RustEvalAction::Done { result } => eval_done_action(result),
931
+ }
932
+ }
933
+
934
+ /// Phase 2 — feed back the evaluator LLM's text response.
935
+ #[napi]
936
+ pub fn feed_eval_result(&mut self, content: String) -> EvalPipelineAction {
937
+ match self.inner.feed(RustEvalEvent::EvalResult { content }) {
938
+ RustEvalAction::Done { result } => eval_done_action(result),
939
+ RustEvalAction::Evaluate { messages } => EvalPipelineAction {
940
+ kind: "evaluate".into(),
941
+ messages: Some(messages.iter().map(message_from_rust).collect()),
942
+ passed: None,
943
+ feedback: None,
944
+ skill_candidate: None,
945
+ },
946
+ }
947
+ }
948
+
949
+ #[napi]
950
+ pub fn reset(&mut self) {
951
+ self.inner.reset();
952
+ }
953
+
954
+ #[napi]
955
+ pub fn is_idle(&self) -> bool {
956
+ self.inner.is_idle()
957
+ }
958
+ }
959
+
960
+ fn eval_done_action(result: deepstrike_core::harness::eval_pipeline::EvalResult) -> EvalPipelineAction {
961
+ EvalPipelineAction {
962
+ kind: "done".into(),
963
+ messages: None,
964
+ passed: Some(result.passed),
965
+ feedback: Some(result.feedback),
966
+ skill_candidate: result.skill_candidate.map(|s| SkillCandidate {
967
+ name: s.name,
968
+ description: s.description,
969
+ when_to_use: s.when_to_use,
970
+ content: s.content,
971
+ }),
972
+ }
973
+ }
974
+
975
+ /// Kernel state machine for the idle dreaming cycle.
976
+ ///
977
+ /// Drive it like this:
978
+ /// 1. `feedTrigger(sessions, existingMemories, nowMs)` → `"synthesize_insights"` action
979
+ /// 2. Call LLM with `action.messages`, collect the text response
980
+ /// 3. `feedSynthesisResult(text)` → `"commit_memories"` action
981
+ /// 4. Apply `action.curationResult` via `DreamStore.commit`, then call `reset()`
982
+ #[napi]
983
+ pub struct IdlePipeline {
984
+ inner: RustIdlePipeline,
985
+ }
986
+
987
+ #[napi]
988
+ impl IdlePipeline {
989
+ #[napi(constructor)]
990
+ pub fn new(agent_id: String) -> Self {
991
+ Self { inner: RustIdlePipeline::new(RustIdlePolicy::new(agent_id)) }
992
+ }
993
+
994
+ /// Phase 1 — provide sessions + current memory snapshot; kernel builds the LLM prompt.
995
+ #[napi]
996
+ pub fn feed_trigger(
997
+ &mut self,
998
+ sessions: Vec<SessionData>,
999
+ existing_memories: Vec<MemoryEntry>,
1000
+ now_ms: f64,
1001
+ ) -> Result<IdlePipelineAction> {
1002
+ let rust_sessions: Vec<RustSessionData> =
1003
+ sessions.into_iter().map(session_data_to_rust).collect::<Result<_>>()?;
1004
+ let rust_memories: Vec<RustMemoryEntry> =
1005
+ existing_memories.into_iter().map(memory_entry_to_rust).collect();
1006
+ let action = self.inner.feed(RustIdleEvent::Trigger {
1007
+ sessions: rust_sessions,
1008
+ existing_memories: rust_memories,
1009
+ now_ms: now_ms as u64,
1010
+ });
1011
+ Ok(idle_pipeline_action_from_rust(action))
1012
+ }
1013
+
1014
+ /// Phase 2 — feed back the LLM's synthesis text; kernel parses and curates.
1015
+ #[napi]
1016
+ pub fn feed_synthesis_result(&mut self, content: String) -> IdlePipelineAction {
1017
+ idle_pipeline_action_from_rust(self.inner.feed(RustIdleEvent::SynthesisResult { content }))
1018
+ }
1019
+
1020
+ #[napi]
1021
+ pub fn is_idle(&self) -> bool {
1022
+ self.inner.is_idle()
1023
+ }
1024
+
1025
+ /// Reset to `Idle` after handling `CommitMemories` to allow the next cycle.
1026
+ #[napi]
1027
+ pub fn reset(&mut self) {
1028
+ self.inner.reset();
1029
+ }
1030
+ }