@deepstrike/core 0.1.1 → 0.1.4

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 CHANGED
@@ -20,5 +20,3 @@ serde_json = { workspace = true }
20
20
  [build-dependencies]
21
21
  napi-build = "2"
22
22
 
23
- [profile.release]
24
- lto = true
package/index.d.ts CHANGED
@@ -0,0 +1,337 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ export interface ContentPartObj {
7
+ /** `"text"` | `"image"` | `"audio"` | `"tool_result"` */
8
+ type: string
9
+ text?: string
10
+ url?: string
11
+ data?: string
12
+ mediaType?: string
13
+ detail?: string
14
+ callId?: string
15
+ output?: string
16
+ isError?: boolean
17
+ }
18
+ export interface Message {
19
+ role: string
20
+ /**
21
+ * Plain-text content. When `content_parts` is present, this holds only the
22
+ * concatenated text segments for backward compatibility.
23
+ */
24
+ content: string
25
+ /** Structured multimodal content parts. When present, takes precedence over `content`. */
26
+ contentParts?: Array<ContentPartObj>
27
+ tokenCount?: number
28
+ toolCalls: Array<ToolCall>
29
+ }
30
+ export interface ToolCall {
31
+ id: string
32
+ name: string
33
+ /** JSON-encoded arguments. JS: `JSON.stringify(args)`. */
34
+ arguments: string
35
+ }
36
+ export interface ToolResult {
37
+ callId: string
38
+ output: string
39
+ isError: boolean
40
+ tokenCount?: number
41
+ }
42
+ export interface ToolSchema {
43
+ name: string
44
+ description: string
45
+ /** JSON-encoded JSON Schema. JS: `JSON.stringify(schema)`. */
46
+ parameters: string
47
+ }
48
+ export interface RuntimeTask {
49
+ goal: string
50
+ criteria?: Array<string>
51
+ }
52
+ export interface LoopPolicy {
53
+ maxTokens: number
54
+ maxTurns?: number
55
+ maxTotalTokens?: bigint
56
+ timeoutMs?: bigint
57
+ }
58
+ export interface LoopResult {
59
+ termination: string
60
+ finalMessage?: Message
61
+ turnsUsed: number
62
+ totalTokensUsed: bigint
63
+ }
64
+ /** Unified RuntimeSignal exposed to Node.js — mirrors the kernel type. */
65
+ export interface RuntimeSignal {
66
+ id: string
67
+ /** "cron" | "gateway" | "heartbeat" | "custom" */
68
+ source: string
69
+ /** "event" | "job" | "alert" */
70
+ signalType: string
71
+ /** "low" | "normal" | "high" | "critical" */
72
+ urgency: string
73
+ summary: string
74
+ /** JSON-encoded payload. */
75
+ payload: string
76
+ dedupeKey?: string
77
+ timestampMs: number
78
+ }
79
+ export interface SkillMetadata {
80
+ name: string
81
+ description: string
82
+ whenToUse?: string
83
+ allowedTools?: Array<string>
84
+ effort?: number
85
+ estimatedTokens: number
86
+ }
87
+ /**
88
+ * Discriminated union. Inspect `kind`:
89
+ * - `"call_llm"` → `messages`, `tools` (includes `skill` meta-tool when skills are registered)
90
+ * - `"execute_tools"` → `calls`
91
+ * - `"done"` → `result`
92
+ */
93
+ export interface LoopAction {
94
+ kind: string
95
+ messages?: Array<Message>
96
+ tools?: Array<ToolSchema>
97
+ calls?: Array<ToolCall>
98
+ result?: LoopResult
99
+ }
100
+ /**
101
+ * Discriminated union for observations:
102
+ * - `"compressed"` → `action`, `rho_after`
103
+ */
104
+ export interface LoopObservation {
105
+ kind: string
106
+ action?: string
107
+ rhoAfter?: number
108
+ }
109
+ /** JS-friendly governance verdict returned by `Governance.evaluate`. */
110
+ export interface GovernanceVerdictObj {
111
+ /** `"allow"` | `"deny"` | `"rate_limited"` | `"ask_user"` */
112
+ kind: string
113
+ reason?: string
114
+ /** Milliseconds until the tool may be retried. Only set when `kind === "rate_limited"`. */
115
+ retryAfterMs?: number
116
+ }
117
+ /** A single session of agent messages, used as input to `IdlePipeline.feedTrigger`. */
118
+ export interface SessionData {
119
+ sessionId: string
120
+ agentId: string
121
+ /** Messages from this session. */
122
+ messages: Array<Message>
123
+ /** JSON-encoded metadata blob. */
124
+ metadata: string
125
+ /** Unix ms timestamp. */
126
+ createdAtMs: number
127
+ /** Unix ms timestamp. */
128
+ updatedAtMs: number
129
+ }
130
+ /** A long-term memory entry as stored by the agent. */
131
+ export interface MemoryEntry {
132
+ text: string
133
+ score: number
134
+ /** JSON-encoded metadata blob. */
135
+ metadata: string
136
+ }
137
+ export interface CurationStats {
138
+ insightsProcessed: number
139
+ duplicatesRemoved: number
140
+ conflictsResolved: number
141
+ entriesAdded: number
142
+ }
143
+ /** The delta the `DreamStore.commit` must apply: add `toAdd`, remove `toRemoveIndices`. */
144
+ export interface CurationResult {
145
+ toAdd: Array<MemoryEntry>
146
+ /** Indices into the `existingMemories` slice passed to `feedTrigger`. */
147
+ toRemoveIndices: Array<number>
148
+ stats: CurationStats
149
+ }
150
+ export interface IdleRunResult {
151
+ sessionsProcessed: number
152
+ insightsExtracted: number
153
+ }
154
+ /**
155
+ * Discriminated union returned by `IdlePipeline` methods. Inspect `kind`:
156
+ * - `"synthesize_insights"` → `messages` (SDK must call LLM, then `feedSynthesisResult`)
157
+ * - `"commit_memories"` → `agentId`, `curationResult`, `runResult`
158
+ * - `"noop"` | `"aborted"`
159
+ */
160
+ export interface IdlePipelineAction {
161
+ kind: string
162
+ messages?: Array<Message>
163
+ agentId?: string
164
+ curationResult?: CurationResult
165
+ runResult?: IdleRunResult
166
+ }
167
+ export interface Criterion {
168
+ text: string
169
+ required: boolean
170
+ weight?: number
171
+ }
172
+ export interface CriterionResult {
173
+ criterion: string
174
+ passed: boolean
175
+ score: number
176
+ feedback: string
177
+ }
178
+ export interface EvalPipelineOptions {
179
+ extractSkillOnPass?: boolean
180
+ }
181
+ export interface SkillCandidate {
182
+ name: string
183
+ description: string
184
+ whenToUse?: string
185
+ content: string
186
+ }
187
+ /**
188
+ * Discriminated union returned by `EvalPipeline` methods. Inspect `kind`:
189
+ * - `"evaluate"` → `messages` (SDK must call evaluator LLM, then `feedEvalResult`)
190
+ * - `"done"` → `passed`, `overallScore`, `feedback`, `details`, optional `skillCandidate`
191
+ */
192
+ export interface EvalPipelineAction {
193
+ kind: string
194
+ messages?: Array<Message>
195
+ passed?: boolean
196
+ overallScore?: number
197
+ feedback?: string
198
+ details?: Array<CriterionResult>
199
+ skillCandidate?: SkillCandidate
200
+ }
201
+ export declare class ContextEngine {
202
+ constructor(maxTokens: number)
203
+ addSystemMessage(content: string, tokens: number): void
204
+ addUserMessage(content: string, tokens: number): void
205
+ addAssistantMessage(content: string, tokens: number): void
206
+ pressure(): number
207
+ totalTokens(): number
208
+ /**
209
+ * Run compression at the level the current pressure recommends.
210
+ * Returns tokens saved.
211
+ */
212
+ compress(): number
213
+ render(): Array<Message>
214
+ /**
215
+ * Replace the available-skills set with frontmatter-only metadata.
216
+ * The kernel will auto-inject the `skill` meta-tool into every `CallLLM` action.
217
+ */
218
+ setAvailableSkills(skills: Array<SkillMetadata>): void
219
+ }
220
+ export declare class LoopStateMachine {
221
+ constructor(policy: LoopPolicy)
222
+ /**
223
+ * Convenience: register skills directly on the state machine without
224
+ * reaching into the inner ContextEngine.
225
+ */
226
+ setAvailableSkills(skills: Array<SkillMetadata>): void
227
+ /**
228
+ * Enable the `memory` meta-tool. Call with `true` when a DreamStore and agentId
229
+ * are configured — the SDK layer intercepts `memory` tool calls and runs the search.
230
+ */
231
+ setMemoryEnabled(enabled: boolean): void
232
+ /**
233
+ * Enable the `knowledge` meta-tool. Call with `true` when a KnowledgeSource
234
+ * is configured — the SDK layer intercepts `knowledge` tool calls and runs retrieval.
235
+ */
236
+ setKnowledgeEnabled(enabled: boolean): void
237
+ /**
238
+ * Prepend a system-level instruction to the context. Must be called before `start`.
239
+ * `tokens` is a caller-supplied estimate (use `content.length / 4` if unsure).
240
+ * The renderer skips messages with `tokens == 0`, so always pass at least 1.
241
+ */
242
+ addSystemMessage(content: string, tokens: number): void
243
+ /**
244
+ * Pre-populate the memory partition with a long-term memory snippet.
245
+ * Must be called before `start`. Use for seeding known context from past sessions.
246
+ * `tokens` is a caller-supplied estimate; pass at least 1.
247
+ */
248
+ addMemoryMessage(content: string, tokens: number): void
249
+ setTools(tools: Array<ToolSchema>): void
250
+ start(task: RuntimeTask): LoopAction
251
+ feedLlmResponse(message: Message): LoopAction
252
+ feedToolResults(results: Array<ToolResult>): LoopAction
253
+ feedTimeout(): LoopAction
254
+ isTerminal(): boolean
255
+ turn(): number
256
+ pressure(): number
257
+ /** Drain observations emitted during the most recent feed call. */
258
+ takeObservations(): Array<LoopObservation>
259
+ render(): Array<Message>
260
+ }
261
+ export declare class SignalRouter {
262
+ constructor(maxQueueSize: number)
263
+ /**
264
+ * Ingest a signal. Returns the disposition string:
265
+ * "ignore" | "observe" | "queue" | "run" | "interrupt" | "interrupt_now" | "dropped"
266
+ */
267
+ ingest(signal: RuntimeSignal, isRunning: boolean): string
268
+ /** Pull the next queued signal (highest priority first). */
269
+ next(): RuntimeSignal | null
270
+ depth(): number
271
+ clearDedup(): void
272
+ }
273
+ export declare class Governance {
274
+ /**
275
+ * Create a governance pipeline.
276
+ * `defaultAction` controls the fallback when no rule matches: `"allow"` (default) or `"deny"`.
277
+ */
278
+ constructor(defaultAction?: string | undefined | null)
279
+ /** Set the agent identity used in governance audit logs. */
280
+ setIdentity(agentId: string, sessionId: string): void
281
+ /**
282
+ * Add a permission rule. `pattern` supports globs: `"db.*"`, `"*.delete"`, `"*"`, or exact names.
283
+ * `action`: `"allow"` | `"deny"` | `"ask_user"`.
284
+ * Rules are evaluated in insertion order; first match wins.
285
+ */
286
+ addPermissionRule(pattern: string, action: string): void
287
+ /** Hard-block a tool name (veto stage — cannot be overridden by permission rules). */
288
+ blockTool(name: string): void
289
+ /** Advance the internal clock used by rate limiting and audit. */
290
+ setTime(nowMs: bigint): void
291
+ /**
292
+ * Evaluate a tool call through the full pipeline (Permission → Veto → RateLimit → Constraint → Audit).
293
+ * `argsJson`: JSON-encoded tool arguments string.
294
+ */
295
+ evaluate(toolName: string, argsJson: string): GovernanceVerdictObj
296
+ }
297
+ /**
298
+ * Kernel state machine for the evaluation cycle.
299
+ *
300
+ * Drive it like this:
301
+ * 1. `feedOutcome(goal, criteria, result, attempt)` → `"evaluate"` action
302
+ * 2. Call evaluator LLM with `action.messages`, collect the text response
303
+ * 3. `feedEvalResult(text)` → `"done"` action
304
+ * 4. Read `action.passed` / `action.feedback` / `action.skillCandidate`
305
+ * 5. Call `reset()` before the next attempt
306
+ */
307
+ export declare class EvalPipeline {
308
+ constructor(options?: EvalPipelineOptions | undefined | null)
309
+ /**
310
+ * Phase 1 — provide the goal, criteria, agent output, and attempt number.
311
+ * Returns an `"evaluate"` action with messages to send to the evaluator LLM.
312
+ */
313
+ feedOutcome(goal: string, criteria: Array<Criterion>, result: string, attempt: number): EvalPipelineAction
314
+ /** Phase 2 — feed back the evaluator LLM's text response. */
315
+ feedEvalResult(content: string): EvalPipelineAction
316
+ reset(): void
317
+ isIdle(): boolean
318
+ }
319
+ /**
320
+ * Kernel state machine for the idle dreaming cycle.
321
+ *
322
+ * Drive it like this:
323
+ * 1. `feedTrigger(sessions, existingMemories, nowMs)` → `"synthesize_insights"` action
324
+ * 2. Call LLM with `action.messages`, collect the text response
325
+ * 3. `feedSynthesisResult(text)` → `"commit_memories"` action
326
+ * 4. Apply `action.curationResult` via `DreamStore.commit`, then call `reset()`
327
+ */
328
+ export declare class IdlePipeline {
329
+ constructor(agentId: string)
330
+ /** Phase 1 — provide sessions + current memory snapshot; kernel builds the LLM prompt. */
331
+ feedTrigger(sessions: Array<SessionData>, existingMemories: Array<MemoryEntry>, nowMs: number): IdlePipelineAction
332
+ /** Phase 2 — feed back the LLM's synthesis text; kernel parses and curates. */
333
+ feedSynthesisResult(content: string): IdlePipelineAction
334
+ isIdle(): boolean
335
+ /** Reset to `Idle` after handling `CommitMemories` to allow the next cycle. */
336
+ reset(): void
337
+ }
package/index.js CHANGED
@@ -1,5 +1,8 @@
1
- /* auto-generated by napi-rs */
2
- 'use strict'
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /* prettier-ignore */
4
+
5
+ /* auto-generated by NAPI-RS */
3
6
 
4
7
  const { existsSync, readFileSync } = require('fs')
5
8
  const { join } = require('path')
@@ -11,55 +14,286 @@ let localFileExisted = false
11
14
  let loadError = null
12
15
 
13
16
  function isMusl() {
14
- if (!existsSync('/usr/bin/ldd')) return true
15
- return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
17
+ // For Node 10
18
+ if (!process.report || typeof process.report.getReport !== 'function') {
19
+ try {
20
+ const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
+ return readFileSync(lddPath, 'utf8').includes('musl')
22
+ } catch (e) {
23
+ return true
24
+ }
25
+ } else {
26
+ const { glibcVersionRuntime } = process.report.getReport().header
27
+ return !glibcVersionRuntime
28
+ }
16
29
  }
17
30
 
18
31
  switch (platform) {
19
32
  case 'android':
20
33
  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}`)
34
+ case 'arm64':
35
+ localFileExisted = existsSync(join(__dirname, 'index.android-arm64.node'))
36
+ try {
37
+ if (localFileExisted) {
38
+ nativeBinding = require('./index.android-arm64.node')
39
+ } else {
40
+ nativeBinding = require('@deepstrike/core-android-arm64')
41
+ }
42
+ } catch (e) {
43
+ loadError = e
44
+ }
45
+ break
46
+ case 'arm':
47
+ localFileExisted = existsSync(join(__dirname, 'index.android-arm-eabi.node'))
48
+ try {
49
+ if (localFileExisted) {
50
+ nativeBinding = require('./index.android-arm-eabi.node')
51
+ } else {
52
+ nativeBinding = require('@deepstrike/core-android-arm-eabi')
53
+ }
54
+ } catch (e) {
55
+ loadError = e
56
+ }
57
+ break
58
+ default:
59
+ throw new Error(`Unsupported architecture on Android ${arch}`)
24
60
  }
25
61
  break
26
62
  case 'win32':
27
63
  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}`)
64
+ case 'x64':
65
+ localFileExisted = existsSync(
66
+ join(__dirname, 'index.win32-x64-msvc.node')
67
+ )
68
+ try {
69
+ if (localFileExisted) {
70
+ nativeBinding = require('./index.win32-x64-msvc.node')
71
+ } else {
72
+ nativeBinding = require('@deepstrike/core-win32-x64-msvc')
73
+ }
74
+ } catch (e) {
75
+ loadError = e
76
+ }
77
+ break
78
+ case 'ia32':
79
+ localFileExisted = existsSync(
80
+ join(__dirname, 'index.win32-ia32-msvc.node')
81
+ )
82
+ try {
83
+ if (localFileExisted) {
84
+ nativeBinding = require('./index.win32-ia32-msvc.node')
85
+ } else {
86
+ nativeBinding = require('@deepstrike/core-win32-ia32-msvc')
87
+ }
88
+ } catch (e) {
89
+ loadError = e
90
+ }
91
+ break
92
+ case 'arm64':
93
+ localFileExisted = existsSync(
94
+ join(__dirname, 'index.win32-arm64-msvc.node')
95
+ )
96
+ try {
97
+ if (localFileExisted) {
98
+ nativeBinding = require('./index.win32-arm64-msvc.node')
99
+ } else {
100
+ nativeBinding = require('@deepstrike/core-win32-arm64-msvc')
101
+ }
102
+ } catch (e) {
103
+ loadError = e
104
+ }
105
+ break
106
+ default:
107
+ throw new Error(`Unsupported architecture on Windows: ${arch}`)
32
108
  }
33
109
  break
34
110
  case 'darwin':
35
- localFileExisted = existsSync(join(__dirname, 'deepstrike-core.darwin-universal.node'))
111
+ localFileExisted = existsSync(join(__dirname, 'index.darwin-universal.node'))
36
112
  try {
37
- if (localFileExisted) { nativeBinding = require('./deepstrike-core.darwin-universal.node'); break }
113
+ if (localFileExisted) {
114
+ nativeBinding = require('./index.darwin-universal.node')
115
+ } else {
116
+ nativeBinding = require('@deepstrike/core-darwin-universal')
117
+ }
118
+ break
38
119
  } catch {}
39
120
  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}`)
121
+ case 'x64':
122
+ localFileExisted = existsSync(join(__dirname, 'index.darwin-x64.node'))
123
+ try {
124
+ if (localFileExisted) {
125
+ nativeBinding = require('./index.darwin-x64.node')
126
+ } else {
127
+ nativeBinding = require('@deepstrike/core-darwin-x64')
128
+ }
129
+ } catch (e) {
130
+ loadError = e
131
+ }
132
+ break
133
+ case 'arm64':
134
+ localFileExisted = existsSync(
135
+ join(__dirname, 'index.darwin-arm64.node')
136
+ )
137
+ try {
138
+ if (localFileExisted) {
139
+ nativeBinding = require('./index.darwin-arm64.node')
140
+ } else {
141
+ nativeBinding = require('@deepstrike/core-darwin-arm64')
142
+ }
143
+ } catch (e) {
144
+ loadError = e
145
+ }
146
+ break
147
+ default:
148
+ throw new Error(`Unsupported architecture on macOS: ${arch}`)
43
149
  }
44
150
  break
45
151
  case 'freebsd':
46
- if (arch !== 'x64') throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
47
- nativeBinding = require('@deepstrike/core-freebsd-x64')
152
+ if (arch !== 'x64') {
153
+ throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
+ }
155
+ localFileExisted = existsSync(join(__dirname, 'index.freebsd-x64.node'))
156
+ try {
157
+ if (localFileExisted) {
158
+ nativeBinding = require('./index.freebsd-x64.node')
159
+ } else {
160
+ nativeBinding = require('@deepstrike/core-freebsd-x64')
161
+ }
162
+ } catch (e) {
163
+ loadError = e
164
+ }
48
165
  break
49
166
  case 'linux':
50
167
  switch (arch) {
51
168
  case 'x64':
52
- nativeBinding = isMusl()
53
- ? require('@deepstrike/core-linux-x64-musl')
54
- : require('@deepstrike/core-linux-x64-gnu')
169
+ if (isMusl()) {
170
+ localFileExisted = existsSync(
171
+ join(__dirname, 'index.linux-x64-musl.node')
172
+ )
173
+ try {
174
+ if (localFileExisted) {
175
+ nativeBinding = require('./index.linux-x64-musl.node')
176
+ } else {
177
+ nativeBinding = require('@deepstrike/core-linux-x64-musl')
178
+ }
179
+ } catch (e) {
180
+ loadError = e
181
+ }
182
+ } else {
183
+ localFileExisted = existsSync(
184
+ join(__dirname, 'index.linux-x64-gnu.node')
185
+ )
186
+ try {
187
+ if (localFileExisted) {
188
+ nativeBinding = require('./index.linux-x64-gnu.node')
189
+ } else {
190
+ nativeBinding = require('@deepstrike/core-linux-x64-gnu')
191
+ }
192
+ } catch (e) {
193
+ loadError = e
194
+ }
195
+ }
55
196
  break
56
197
  case 'arm64':
57
- nativeBinding = isMusl()
58
- ? require('@deepstrike/core-linux-arm64-musl')
59
- : require('@deepstrike/core-linux-arm64-gnu')
198
+ if (isMusl()) {
199
+ localFileExisted = existsSync(
200
+ join(__dirname, 'index.linux-arm64-musl.node')
201
+ )
202
+ try {
203
+ if (localFileExisted) {
204
+ nativeBinding = require('./index.linux-arm64-musl.node')
205
+ } else {
206
+ nativeBinding = require('@deepstrike/core-linux-arm64-musl')
207
+ }
208
+ } catch (e) {
209
+ loadError = e
210
+ }
211
+ } else {
212
+ localFileExisted = existsSync(
213
+ join(__dirname, 'index.linux-arm64-gnu.node')
214
+ )
215
+ try {
216
+ if (localFileExisted) {
217
+ nativeBinding = require('./index.linux-arm64-gnu.node')
218
+ } else {
219
+ nativeBinding = require('@deepstrike/core-linux-arm64-gnu')
220
+ }
221
+ } catch (e) {
222
+ loadError = e
223
+ }
224
+ }
60
225
  break
61
226
  case 'arm':
62
- nativeBinding = require('@deepstrike/core-linux-arm-gnueabihf')
227
+ if (isMusl()) {
228
+ localFileExisted = existsSync(
229
+ join(__dirname, 'index.linux-arm-musleabihf.node')
230
+ )
231
+ try {
232
+ if (localFileExisted) {
233
+ nativeBinding = require('./index.linux-arm-musleabihf.node')
234
+ } else {
235
+ nativeBinding = require('@deepstrike/core-linux-arm-musleabihf')
236
+ }
237
+ } catch (e) {
238
+ loadError = e
239
+ }
240
+ } else {
241
+ localFileExisted = existsSync(
242
+ join(__dirname, 'index.linux-arm-gnueabihf.node')
243
+ )
244
+ try {
245
+ if (localFileExisted) {
246
+ nativeBinding = require('./index.linux-arm-gnueabihf.node')
247
+ } else {
248
+ nativeBinding = require('@deepstrike/core-linux-arm-gnueabihf')
249
+ }
250
+ } catch (e) {
251
+ loadError = e
252
+ }
253
+ }
254
+ break
255
+ case 'riscv64':
256
+ if (isMusl()) {
257
+ localFileExisted = existsSync(
258
+ join(__dirname, 'index.linux-riscv64-musl.node')
259
+ )
260
+ try {
261
+ if (localFileExisted) {
262
+ nativeBinding = require('./index.linux-riscv64-musl.node')
263
+ } else {
264
+ nativeBinding = require('@deepstrike/core-linux-riscv64-musl')
265
+ }
266
+ } catch (e) {
267
+ loadError = e
268
+ }
269
+ } else {
270
+ localFileExisted = existsSync(
271
+ join(__dirname, 'index.linux-riscv64-gnu.node')
272
+ )
273
+ try {
274
+ if (localFileExisted) {
275
+ nativeBinding = require('./index.linux-riscv64-gnu.node')
276
+ } else {
277
+ nativeBinding = require('@deepstrike/core-linux-riscv64-gnu')
278
+ }
279
+ } catch (e) {
280
+ loadError = e
281
+ }
282
+ }
283
+ break
284
+ case 's390x':
285
+ localFileExisted = existsSync(
286
+ join(__dirname, 'index.linux-s390x-gnu.node')
287
+ )
288
+ try {
289
+ if (localFileExisted) {
290
+ nativeBinding = require('./index.linux-s390x-gnu.node')
291
+ } else {
292
+ nativeBinding = require('@deepstrike/core-linux-s390x-gnu')
293
+ }
294
+ } catch (e) {
295
+ loadError = e
296
+ }
63
297
  break
64
298
  default:
65
299
  throw new Error(`Unsupported architecture on Linux: ${arch}`)
@@ -69,4 +303,18 @@ switch (platform) {
69
303
  throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
70
304
  }
71
305
 
72
- module.exports = nativeBinding
306
+ if (!nativeBinding) {
307
+ if (loadError) {
308
+ throw loadError
309
+ }
310
+ throw new Error(`Failed to load native binding`)
311
+ }
312
+
313
+ const { ContextEngine, LoopStateMachine, SignalRouter, Governance, EvalPipeline, IdlePipeline } = nativeBinding
314
+
315
+ module.exports.ContextEngine = ContextEngine
316
+ module.exports.LoopStateMachine = LoopStateMachine
317
+ module.exports.SignalRouter = SignalRouter
318
+ module.exports.Governance = Governance
319
+ module.exports.EvalPipeline = EvalPipeline
320
+ module.exports.IdlePipeline = IdlePipeline
@@ -1,3 +1,22 @@
1
1
  # `@deepstrike/core-darwin-arm64`
2
2
 
3
- This is the **aarch64-apple-darwin** binary for `@deepstrike/core`
3
+ Platform-specific native addon for [`@deepstrike/core`](https://www.npmjs.com/package/@deepstrike/core).
4
+
5
+ - **Platform:** macOS ARM64 (Apple Silicon)
6
+ - **Target triple:** `aarch64-apple-darwin`
7
+
8
+ ## Do not install directly
9
+
10
+ This package is an internal binary dependency. Install [`@deepstrike/sdk`](https://www.npmjs.com/package/@deepstrike/sdk) instead — the correct platform package is selected and installed automatically via `optionalDependencies`.
11
+
12
+ ```bash
13
+ npm install @deepstrike/sdk
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ `@deepstrike/core` loads this package at runtime when running on macOS with Apple Silicon. The `.node` file is a compiled Rust extension built with [napi-rs](https://napi.rs) that exposes the DeepStrike kernel (loop control, context compression, governance, signal routing) to Node.js.
19
+
20
+ ## License
21
+
22
+ Apache-2.0 OR MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core-darwin-arm64",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "cpu": [
5
5
  "arm64"
6
6
  ],
@@ -1,3 +1,22 @@
1
1
  # `@deepstrike/core-darwin-x64`
2
2
 
3
- This is the **x86_64-apple-darwin** binary for `@deepstrike/core`
3
+ Platform-specific native addon for [`@deepstrike/core`](https://www.npmjs.com/package/@deepstrike/core).
4
+
5
+ - **Platform:** macOS x64 (Intel)
6
+ - **Target triple:** `x86_64-apple-darwin`
7
+
8
+ ## Do not install directly
9
+
10
+ This package is an internal binary dependency. Install [`@deepstrike/sdk`](https://www.npmjs.com/package/@deepstrike/sdk) instead — the correct platform package is selected and installed automatically via `optionalDependencies`.
11
+
12
+ ```bash
13
+ npm install @deepstrike/sdk
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ `@deepstrike/core` loads this package at runtime when running on macOS with an Intel processor. The `.node` file is a compiled Rust extension built with [napi-rs](https://napi.rs) that exposes the DeepStrike kernel (loop control, context compression, governance, signal routing) to Node.js.
19
+
20
+ ## License
21
+
22
+ Apache-2.0 OR MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core-darwin-x64",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "cpu": [
5
5
  "x64"
6
6
  ],
@@ -1,3 +1,22 @@
1
1
  # `@deepstrike/core-linux-arm64-gnu`
2
2
 
3
- This is the **aarch64-unknown-linux-gnu** binary for `@deepstrike/core`
3
+ Platform-specific native addon for [`@deepstrike/core`](https://www.npmjs.com/package/@deepstrike/core).
4
+
5
+ - **Platform:** Linux ARM64 (glibc)
6
+ - **Target triple:** `aarch64-unknown-linux-gnu`
7
+
8
+ ## Do not install directly
9
+
10
+ This package is an internal binary dependency. Install [`@deepstrike/sdk`](https://www.npmjs.com/package/@deepstrike/sdk) instead — the correct platform package is selected and installed automatically via `optionalDependencies`.
11
+
12
+ ```bash
13
+ npm install @deepstrike/sdk
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ `@deepstrike/core` loads this package at runtime when running on Linux ARM64 with glibc (e.g. AWS Graviton, Raspberry Pi OS 64-bit). The `.node` file is a compiled Rust extension built with [napi-rs](https://napi.rs) that exposes the DeepStrike kernel (loop control, context compression, governance, signal routing) to Node.js.
19
+
20
+ ## License
21
+
22
+ Apache-2.0 OR MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core-linux-arm64-gnu",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "cpu": [
5
5
  "arm64"
6
6
  ],
@@ -1,3 +1,22 @@
1
1
  # `@deepstrike/core-linux-arm64-musl`
2
2
 
3
- This is the **aarch64-unknown-linux-musl** binary for `@deepstrike/core`
3
+ Platform-specific native addon for [`@deepstrike/core`](https://www.npmjs.com/package/@deepstrike/core).
4
+
5
+ - **Platform:** Linux ARM64 (musl / Alpine)
6
+ - **Target triple:** `aarch64-unknown-linux-musl`
7
+
8
+ ## Do not install directly
9
+
10
+ This package is an internal binary dependency. Install [`@deepstrike/sdk`](https://www.npmjs.com/package/@deepstrike/sdk) instead — the correct platform package is selected and installed automatically via `optionalDependencies`.
11
+
12
+ ```bash
13
+ npm install @deepstrike/sdk
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ `@deepstrike/core` loads this package at runtime when running on Linux ARM64 with musl libc (e.g. Alpine Linux on ARM). The `.node` file is a compiled Rust extension built with [napi-rs](https://napi.rs) that exposes the DeepStrike kernel (loop control, context compression, governance, signal routing) to Node.js.
19
+
20
+ ## License
21
+
22
+ Apache-2.0 OR MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core-linux-arm64-musl",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "cpu": [
5
5
  "arm64"
6
6
  ],
@@ -1,3 +1,22 @@
1
1
  # `@deepstrike/core-linux-x64-gnu`
2
2
 
3
- This is the **x86_64-unknown-linux-gnu** binary for `@deepstrike/core`
3
+ Platform-specific native addon for [`@deepstrike/core`](https://www.npmjs.com/package/@deepstrike/core).
4
+
5
+ - **Platform:** Linux x64 (glibc)
6
+ - **Target triple:** `x86_64-unknown-linux-gnu`
7
+
8
+ ## Do not install directly
9
+
10
+ This package is an internal binary dependency. Install [`@deepstrike/sdk`](https://www.npmjs.com/package/@deepstrike/sdk) instead — the correct platform package is selected and installed automatically via `optionalDependencies`.
11
+
12
+ ```bash
13
+ npm install @deepstrike/sdk
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ `@deepstrike/core` loads this package at runtime when running on Linux x64 with glibc. The `.node` file is a compiled Rust extension built with [napi-rs](https://napi.rs) that exposes the DeepStrike kernel (loop control, context compression, governance, signal routing) to Node.js.
19
+
20
+ ## License
21
+
22
+ Apache-2.0 OR MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core-linux-x64-gnu",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "cpu": [
5
5
  "x64"
6
6
  ],
@@ -1,3 +1,22 @@
1
1
  # `@deepstrike/core-linux-x64-musl`
2
2
 
3
- This is the **x86_64-unknown-linux-musl** binary for `@deepstrike/core`
3
+ Platform-specific native addon for [`@deepstrike/core`](https://www.npmjs.com/package/@deepstrike/core).
4
+
5
+ - **Platform:** Linux x64 (musl / Alpine)
6
+ - **Target triple:** `x86_64-unknown-linux-musl`
7
+
8
+ ## Do not install directly
9
+
10
+ This package is an internal binary dependency. Install [`@deepstrike/sdk`](https://www.npmjs.com/package/@deepstrike/sdk) instead — the correct platform package is selected and installed automatically via `optionalDependencies`.
11
+
12
+ ```bash
13
+ npm install @deepstrike/sdk
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ `@deepstrike/core` loads this package at runtime when running on Linux x64 with musl libc (e.g. Alpine Linux, Docker scratch images). The `.node` file is a compiled Rust extension built with [napi-rs](https://napi.rs) that exposes the DeepStrike kernel (loop control, context compression, governance, signal routing) to Node.js.
19
+
20
+ ## License
21
+
22
+ Apache-2.0 OR MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core-linux-x64-musl",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "cpu": [
5
5
  "x64"
6
6
  ],
@@ -1,3 +1,22 @@
1
1
  # `@deepstrike/core-win32-x64-msvc`
2
2
 
3
- This is the **x86_64-pc-windows-msvc** binary for `@deepstrike/core`
3
+ Platform-specific native addon for [`@deepstrike/core`](https://www.npmjs.com/package/@deepstrike/core).
4
+
5
+ - **Platform:** Windows x64 (MSVC)
6
+ - **Target triple:** `x86_64-pc-windows-msvc`
7
+
8
+ ## Do not install directly
9
+
10
+ This package is an internal binary dependency. Install [`@deepstrike/sdk`](https://www.npmjs.com/package/@deepstrike/sdk) instead — the correct platform package is selected and installed automatically via `optionalDependencies`.
11
+
12
+ ```bash
13
+ npm install @deepstrike/sdk
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ `@deepstrike/core` loads this package at runtime when running on Windows x64. The `.node` file is a compiled Rust extension built with [napi-rs](https://napi.rs) that exposes the DeepStrike kernel (loop control, context compression, governance, signal routing) to Node.js.
19
+
20
+ ## License
21
+
22
+ Apache-2.0 OR MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core-win32-x64-msvc",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "cpu": [
5
5
  "x64"
6
6
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "DeepStrike kernel — pre-built native addon",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -16,10 +16,10 @@
16
16
  ]
17
17
  },
18
18
  "optionalDependencies": {
19
- "@deepstrike/core-linux-x64-gnu": "0.1.1",
20
- "@deepstrike/core-linux-arm64-gnu": "0.1.1",
21
- "@deepstrike/core-darwin-x64": "0.1.1",
22
- "@deepstrike/core-darwin-arm64": "0.1.1",
23
- "@deepstrike/core-win32-x64-msvc": "0.1.1"
19
+ "@deepstrike/core-linux-x64-gnu": "0.1.4",
20
+ "@deepstrike/core-linux-arm64-gnu": "0.1.4",
21
+ "@deepstrike/core-darwin-x64": "0.1.4",
22
+ "@deepstrike/core-darwin-arm64": "0.1.4",
23
+ "@deepstrike/core-win32-x64-msvc": "0.1.4"
24
24
  }
25
25
  }
package/src/lib.rs CHANGED
@@ -43,10 +43,13 @@ use compact_str::CompactString;
43
43
 
44
44
  use deepstrike_core::context::manager::ContextManager;
45
45
  use deepstrike_core::context::pressure::PressureAction;
46
+ use deepstrike_core::governance::permission::{PermissionAction, PermissionRule};
46
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;
47
50
  use deepstrike_core::harness::eval_pipeline::{
48
- EvalAction as RustEvalAction, EvalEvent as RustEvalEvent, EvalPolicy as RustEvalPolicy,
49
- EvalPipeline as RustEvalPipeline,
51
+ Criterion as RustCriterion, EvalAction as RustEvalAction, EvalEvent as RustEvalEvent,
52
+ EvalPolicy as RustEvalPolicy, EvalPipeline as RustEvalPipeline,
50
53
  };
51
54
  use deepstrike_core::memory::curator::CurationResult as RustCurationResult;
52
55
  use deepstrike_core::memory::durable::SessionData as RustSessionData;
@@ -76,11 +79,30 @@ use deepstrike_core::types::task::RuntimeTask as RustRuntimeTask;
76
79
 
77
80
  // ────────────────────────────────────── POD types (plain JS objects) ──────────────────────────────────────
78
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
+
79
97
  #[napi(object)]
80
98
  #[derive(Clone)]
81
99
  pub struct Message {
82
100
  pub role: String,
101
+ /// Plain-text content. When `content_parts` is present, this holds only the
102
+ /// concatenated text segments for backward compatibility.
83
103
  pub content: String,
104
+ /// Structured multimodal content parts. When present, takes precedence over `content`.
105
+ pub content_parts: Option<Vec<ContentPartObj>>,
84
106
  pub token_count: Option<u32>,
85
107
  pub tool_calls: Vec<ToolCall>,
86
108
  }
@@ -288,6 +310,51 @@ fn role_to_str(role: Role) -> &'static str {
288
310
  }
289
311
  }
290
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
+
291
358
  fn message_to_rust(m: Message) -> Result<RustMessage> {
292
359
  let role = role_str_to_rust(&m.role)?;
293
360
  let tool_calls: Vec<RustToolCall> = m
@@ -295,32 +362,29 @@ fn message_to_rust(m: Message) -> Result<RustMessage> {
295
362
  .into_iter()
296
363
  .map(tool_call_to_rust)
297
364
  .collect::<Result<_>>()?;
298
- Ok(RustMessage {
299
- role,
300
- content: Content::Text(m.content),
301
- tool_calls,
302
- token_count: m.token_count,
303
- })
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 })
304
370
  }
305
371
 
306
372
  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"),
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
+ }
320
383
  };
321
384
  Message {
322
385
  role: role_to_str(m.role).to_string(),
323
386
  content,
387
+ content_parts,
324
388
  token_count: m.token_count,
325
389
  tool_calls: m.tool_calls.iter().map(tool_call_from_rust).collect(),
326
390
  }
@@ -577,6 +641,30 @@ impl LoopStateMachine {
577
641
  self.inner.ctx.set_knowledge_enabled(enabled);
578
642
  }
579
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
+
580
668
  #[napi]
581
669
  pub fn set_tools(&mut self, tools: Vec<ToolSchema>) -> Result<()> {
582
670
  let rust_tools: Vec<RustToolSchema> = tools
@@ -683,27 +771,100 @@ impl SignalRouter {
683
771
 
684
772
  // ─────────────────────────────────────────── Governance ───────────────────────────────────────────
685
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
+
686
794
  #[napi]
687
795
  pub struct Governance {
688
796
  inner: RustGovernancePipeline,
797
+ agent_id: String,
798
+ session_id: String,
689
799
  }
690
800
 
691
801
  #[napi]
692
802
  impl Governance {
803
+ /// Create a governance pipeline.
804
+ /// `defaultAction` controls the fallback when no rule matches: `"allow"` (default) or `"deny"`.
693
805
  #[napi(constructor)]
694
- pub fn new() -> Self {
695
- Self { inner: RustGovernancePipeline::default() }
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;
696
824
  }
697
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).
698
843
  #[napi]
699
844
  pub fn block_tool(&mut self, name: String) {
700
845
  self.inner.veto.block_tool(name);
701
846
  }
702
847
 
848
+ /// Advance the internal clock used by rate limiting and audit.
703
849
  #[napi]
704
850
  pub fn set_time(&mut self, now_ms: BigInt) {
705
851
  self.inner.set_time(now_ms.get_u64().1);
706
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
+ }
707
868
  }
708
869
 
709
870
  // ──────────────────────────────── Dream / idle-pipeline POD types ────────────────────────────────
@@ -856,6 +1017,23 @@ fn idle_pipeline_action_from_rust(a: RustIdleAction) -> IdlePipelineAction {
856
1017
 
857
1018
  // ─────────────────────────────────────────── EvalPipeline ────────────────────────────────────────
858
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
+
859
1037
  #[napi(object)]
860
1038
  #[derive(Clone)]
861
1039
  pub struct EvalPipelineOptions {
@@ -873,14 +1051,16 @@ pub struct SkillCandidate {
873
1051
 
874
1052
  /// Discriminated union returned by `EvalPipeline` methods. Inspect `kind`:
875
1053
  /// - `"evaluate"` → `messages` (SDK must call evaluator LLM, then `feedEvalResult`)
876
- /// - `"done"` → `result` with `passed`, `feedback`, optional `skillCandidate`
1054
+ /// - `"done"` → `passed`, `overallScore`, `feedback`, `details`, optional `skillCandidate`
877
1055
  #[napi(object)]
878
1056
  #[derive(Clone)]
879
1057
  pub struct EvalPipelineAction {
880
1058
  pub kind: String,
881
1059
  pub messages: Option<Vec<Message>>,
882
1060
  pub passed: Option<bool>,
1061
+ pub overall_score: Option<f64>,
883
1062
  pub feedback: Option<String>,
1063
+ pub details: Option<Vec<CriterionResult>>,
884
1064
  pub skill_candidate: Option<SkillCandidate>,
885
1065
  }
886
1066
 
@@ -915,16 +1095,23 @@ impl EvalPipeline {
915
1095
  pub fn feed_outcome(
916
1096
  &mut self,
917
1097
  goal: String,
918
- criteria: Vec<String>,
1098
+ criteria: Vec<Criterion>,
919
1099
  result: String,
920
1100
  attempt: u32,
921
1101
  ) -> EvalPipelineAction {
922
- match self.inner.feed(RustEvalEvent::Outcome { goal, criteria, result, attempt }) {
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 }) {
923
1108
  RustEvalAction::Evaluate { messages } => EvalPipelineAction {
924
1109
  kind: "evaluate".into(),
925
1110
  messages: Some(messages.iter().map(message_from_rust).collect()),
926
1111
  passed: None,
1112
+ overall_score: None,
927
1113
  feedback: None,
1114
+ details: None,
928
1115
  skill_candidate: None,
929
1116
  },
930
1117
  RustEvalAction::Done { result } => eval_done_action(result),
@@ -940,7 +1127,9 @@ impl EvalPipeline {
940
1127
  kind: "evaluate".into(),
941
1128
  messages: Some(messages.iter().map(message_from_rust).collect()),
942
1129
  passed: None,
1130
+ overall_score: None,
943
1131
  feedback: None,
1132
+ details: None,
944
1133
  skill_candidate: None,
945
1134
  },
946
1135
  }
@@ -962,7 +1151,14 @@ fn eval_done_action(result: deepstrike_core::harness::eval_pipeline::EvalResult)
962
1151
  kind: "done".into(),
963
1152
  messages: None,
964
1153
  passed: Some(result.passed),
1154
+ overall_score: Some(result.overall_score as f64),
965
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()),
966
1162
  skill_candidate: result.skill_candidate.map(|s| SkillCandidate {
967
1163
  name: s.name,
968
1164
  description: s.description,