@deepstrike/wasm 0.1.6
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 +299 -0
- package/dist/agent.d.ts +57 -0
- package/dist/agent.js +244 -0
- package/dist/governance.d.ts +14 -0
- package/dist/governance.js +28 -0
- package/dist/harness/index.d.ts +74 -0
- package/dist/harness/index.js +91 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +9 -0
- package/dist/knowledge/index.d.ts +5 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/memory/index.d.ts +56 -0
- package/dist/memory/index.js +8 -0
- package/dist/providers/anthropic.d.ts +8 -0
- package/dist/providers/anthropic.js +89 -0
- package/dist/providers/openai.d.ts +32 -0
- package/dist/providers/openai.js +127 -0
- package/dist/safety/index.d.ts +17 -0
- package/dist/safety/index.js +32 -0
- package/dist/signals/index.d.ts +18 -0
- package/dist/signals/index.js +21 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.js +20 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.js +2 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# DeepStrike WASM SDK
|
|
2
|
+
|
|
3
|
+
Agent framework built on a Rust kernel compiled to WebAssembly. Runs in browsers, Cloudflare Workers, Deno Deploy, and Vercel Edge — anywhere that supports `fetch` and WASM.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @deepstrike/wasm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The Rust kernel is distributed as a pre-built `.wasm` binary (`@deepstrike/wasm-kernel`), which is an indirect dependency — you never import from it directly.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Agent, AnthropicProvider, tool } from "@deepstrike/wasm"
|
|
19
|
+
|
|
20
|
+
const add = tool("add", "Add two numbers.", {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: { x: { type: "number" }, y: { type: "number" } },
|
|
23
|
+
required: ["x", "y"],
|
|
24
|
+
}, async ({ x, y }) => String((x as number) + (y as number)))
|
|
25
|
+
|
|
26
|
+
const agent = new Agent(
|
|
27
|
+
new AnthropicProvider(apiKey),
|
|
28
|
+
{ maxTokens: 32_000, maxTurns: 10 },
|
|
29
|
+
)
|
|
30
|
+
agent.register(add)
|
|
31
|
+
|
|
32
|
+
const answer = await agent.run("What is 2 + 3?")
|
|
33
|
+
console.log(answer) // "5"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Streaming:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
for await (const event of agent.runStreaming("Summarize this page")) {
|
|
40
|
+
if (event.type === "text_delta") process.stdout.write(event.delta)
|
|
41
|
+
else if (event.type === "tool_call") console.log(`\n[→ ${event.name}]`)
|
|
42
|
+
else if (event.type === "done") console.log(`\ndone in ${event.iterations} turns (${event.status})`)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
src/
|
|
52
|
+
├── index.ts # Public exports
|
|
53
|
+
├── agent.ts # Agent — top-level entry point
|
|
54
|
+
├── types.ts # Shared type definitions
|
|
55
|
+
├── providers/ # LLM adapters (fetch-based SSE)
|
|
56
|
+
├── tools/ # tool() helper, executeTools (no fs/shell)
|
|
57
|
+
├── memory/ # WorkingMemory + MemorySource/Extractor interfaces
|
|
58
|
+
├── knowledge/ # KnowledgeSource interface
|
|
59
|
+
├── harness/ # SinglePassHarness, EvalLoopHarness, QualityGate
|
|
60
|
+
├── signals/ # RuntimeSignal, SignalSource, ScheduledPrompt
|
|
61
|
+
└── safety/ # PermissionManager
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The kernel (`@deepstrike/wasm-kernel`, Rust/wasm-bindgen) owns:
|
|
65
|
+
- `LoopStateMachine` — drives `call_llm → execute_tools → load_skills → done`
|
|
66
|
+
- `ContextEngine` — 5-partition context with pressure-based compression
|
|
67
|
+
- `Governance` — tool veto authority
|
|
68
|
+
- `SignalRouter` — external interrupt queue
|
|
69
|
+
|
|
70
|
+
### WASM constraints vs Node SDK
|
|
71
|
+
|
|
72
|
+
| Capability | Browser | Cloudflare Worker | Node |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `fs` read/write | no | no | yes |
|
|
75
|
+
| `bash` tool | no | no | yes |
|
|
76
|
+
| Long-term storage | IndexedDB | KV / D1 | SQLite |
|
|
77
|
+
| External signals | `postMessage` | event | any |
|
|
78
|
+
|
|
79
|
+
The WASM SDK ships **no `readFile` built-in**. Tools must be pure JS / serializable data. Skill loading is delegated to the host (fetch from a URL, read from IndexedDB, etc.).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Providers
|
|
84
|
+
|
|
85
|
+
All providers use `fetch` — no Node.js `http` module.
|
|
86
|
+
|
|
87
|
+
| Class | Backend |
|
|
88
|
+
|-------|---------|
|
|
89
|
+
| `AnthropicProvider` | Anthropic API (SSE) |
|
|
90
|
+
| `OpenAIProvider` | OpenAI API (SSE) |
|
|
91
|
+
| `QwenProvider` | DashScope |
|
|
92
|
+
| `DeepSeekProvider` | DeepSeek API |
|
|
93
|
+
| `MiniMaxProvider` | MiniMax API |
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { AnthropicProvider } from "@deepstrike/wasm"
|
|
97
|
+
|
|
98
|
+
const provider = new AnthropicProvider("sk-...", "claude-opus-4-7")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Thinking / reasoning:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
for await (const event of agent.runStreaming("...", undefined, { enable_thinking: true })) {
|
|
105
|
+
if (event.type === "thinking_delta") console.log(event.delta)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Tools
|
|
112
|
+
|
|
113
|
+
Tools must be pure functions — no shell, no filesystem.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { tool, Agent } from "@deepstrike/wasm"
|
|
117
|
+
|
|
118
|
+
const fetchUrl = tool("fetch_url", "Fetch a URL and return its text.", {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: { url: { type: "string" } },
|
|
121
|
+
required: ["url"],
|
|
122
|
+
}, async ({ url }) => {
|
|
123
|
+
const resp = await fetch(url as string)
|
|
124
|
+
return resp.text()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
agent.register(fetchUrl)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Governance
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { Agent, AnthropicProvider, Governance } from "@deepstrike/wasm"
|
|
136
|
+
|
|
137
|
+
const gov = new Governance()
|
|
138
|
+
gov.blockTool("dangerous_tool")
|
|
139
|
+
|
|
140
|
+
const agent = new Agent(provider, {
|
|
141
|
+
maxTokens: 32_000,
|
|
142
|
+
governance: gov,
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Memory
|
|
149
|
+
|
|
150
|
+
`WorkingMemory` is an in-process scratch pad for within-run state:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { WorkingMemory } from "@deepstrike/wasm"
|
|
154
|
+
|
|
155
|
+
const mem = new WorkingMemory()
|
|
156
|
+
mem.set("step", 1)
|
|
157
|
+
mem.get("step") // 1
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Knowledge
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import type { KnowledgeSource } from "@deepstrike/wasm"
|
|
166
|
+
|
|
167
|
+
class VectorSearch implements KnowledgeSource {
|
|
168
|
+
async retrieve(goal: string, topK = 5): Promise<string[]> {
|
|
169
|
+
const resp = await fetch(`/api/search?q=${encodeURIComponent(goal)}&k=${topK}`)
|
|
170
|
+
return resp.json()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const agent = new Agent(provider, {
|
|
175
|
+
maxTokens: 32_000,
|
|
176
|
+
maxTurns: 10,
|
|
177
|
+
knowledgeSource: new VectorSearch(),
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Harness
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { SinglePassHarness, HarnessLoop } from "@deepstrike/wasm"
|
|
187
|
+
|
|
188
|
+
// Single pass — always passes
|
|
189
|
+
const harness = new SinglePassHarness(agent)
|
|
190
|
+
const outcome = await harness.run({ goal: "Write a haiku" })
|
|
191
|
+
console.log(outcome.result)
|
|
192
|
+
|
|
193
|
+
// Eval loop — LLM-judges the output; retries up to 3 times
|
|
194
|
+
const loop = new HarnessLoop(agent, evalProvider, { maxAttempts: 3 })
|
|
195
|
+
for await (const event of loop.runStreaming({
|
|
196
|
+
goal: "Write a haiku",
|
|
197
|
+
criteria: [
|
|
198
|
+
{ text: "Exactly 3 lines", required: true },
|
|
199
|
+
{ text: "Contains a seasonal reference", required: false },
|
|
200
|
+
],
|
|
201
|
+
})) {
|
|
202
|
+
if (event.type === "done") console.log(event.verdict.passed, event.verdict.overallScore)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Signals & interrupts
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { ScheduledPrompt } from "@deepstrike/wasm"
|
|
212
|
+
import type { SignalSource, RuntimeSignal } from "@deepstrike/wasm"
|
|
213
|
+
|
|
214
|
+
// Interrupt from a UI button
|
|
215
|
+
document.getElementById("stop")!.onclick = () => agent.interrupt()
|
|
216
|
+
|
|
217
|
+
// Convert a scheduled prompt to a RuntimeSignal
|
|
218
|
+
const prompt = new ScheduledPrompt("Daily standup summary", 1_700_000_000_000)
|
|
219
|
+
const signal = prompt.toSignal()
|
|
220
|
+
// signal.source === "cron", signal.signalType === "job"
|
|
221
|
+
|
|
222
|
+
// Feed signals from postMessage (browser) or Cloudflare event
|
|
223
|
+
class PostMessageSource implements SignalSource {
|
|
224
|
+
private queue: RuntimeSignal[] = []
|
|
225
|
+
constructor() {
|
|
226
|
+
self.addEventListener("message", (e: MessageEvent) => {
|
|
227
|
+
if (e.data?.source) this.queue.push(e.data as RuntimeSignal)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
async nextSignal(): Promise<RuntimeSignal | null> {
|
|
231
|
+
return this.queue.shift() ?? null
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Permissions
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { PermissionManager, PermissionMode } from "@deepstrike/wasm"
|
|
242
|
+
|
|
243
|
+
const pm = new PermissionManager(PermissionMode.DEFAULT)
|
|
244
|
+
pm.grant("fetch", "execute")
|
|
245
|
+
pm.grant("storage", "*")
|
|
246
|
+
pm.revoke("fetch", "execute")
|
|
247
|
+
|
|
248
|
+
const decision = pm.evaluate("fetch", "execute")
|
|
249
|
+
decision.allowed // boolean
|
|
250
|
+
decision.reason // string
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Modes: `DEFAULT` (evaluate grants), `PLAN` (block all), `AUTO` (allow all).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Edge runtime examples
|
|
258
|
+
|
|
259
|
+
### Cloudflare Worker
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import init from "@deepstrike/wasm-kernel"
|
|
263
|
+
import { Agent, AnthropicProvider } from "@deepstrike/wasm"
|
|
264
|
+
import wasmBinary from "@deepstrike/wasm-kernel/deepstrike_wasm_bg.wasm"
|
|
265
|
+
|
|
266
|
+
export default {
|
|
267
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
268
|
+
await init(wasmBinary)
|
|
269
|
+
const agent = new Agent(new AnthropicProvider(env.ANTHROPIC_KEY), { maxTokens: 32_000, maxTurns: 10 })
|
|
270
|
+
const result = await agent.run(await request.text())
|
|
271
|
+
return new Response(result)
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Browser (Vite / bundler)
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import init from "@deepstrike/wasm-kernel"
|
|
280
|
+
import { Agent, AnthropicProvider } from "@deepstrike/wasm"
|
|
281
|
+
|
|
282
|
+
await init()
|
|
283
|
+
const agent = new Agent(new AnthropicProvider(import.meta.env.VITE_ANTHROPIC_KEY), { maxTokens: 32_000, maxTurns: 10 })
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Stream events
|
|
289
|
+
|
|
290
|
+
| Event type | Fields |
|
|
291
|
+
|------------|--------|
|
|
292
|
+
| `text_delta` | `delta: string` |
|
|
293
|
+
| `thinking_delta` | `delta: string` |
|
|
294
|
+
| `tool_call` | `id, name, arguments` |
|
|
295
|
+
| `tool_result` | `callId, name, content, isError` |
|
|
296
|
+
| `done` | `iterations, totalTokens, status` |
|
|
297
|
+
| `error` | `message: string` |
|
|
298
|
+
|
|
299
|
+
`status` mirrors the kernel termination reason: `completed` / `max_turns` / `token_budget` / `timeout` / `user_abort` / `error`.
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { LLMProvider, StreamEvent } from "./types.js";
|
|
2
|
+
import type { RegisteredTool } from "./tools/index.js";
|
|
3
|
+
import type { KnowledgeSource } from "./knowledge/index.js";
|
|
4
|
+
import type { SignalSource } from "./signals/index.js";
|
|
5
|
+
import type { DreamStore } from "./memory/index.js";
|
|
6
|
+
import { Governance } from "./governance.js";
|
|
7
|
+
export interface SkillMetadata {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
whenToUse?: string;
|
|
11
|
+
allowedTools?: string[];
|
|
12
|
+
effort?: number;
|
|
13
|
+
estimatedTokens?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface AgentOptions {
|
|
16
|
+
maxTokens: number;
|
|
17
|
+
maxTurns?: number;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
extensions?: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* System-level instructions prepended to every context render.
|
|
22
|
+
* Passed to the kernel's `system` partition before the first LLM call.
|
|
23
|
+
*/
|
|
24
|
+
systemPrompt?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Long-term memory snippets pre-seeded into the context before the first LLM call.
|
|
27
|
+
* Each string is pushed to the kernel's `memory` partition.
|
|
28
|
+
*/
|
|
29
|
+
initialMemory?: string[];
|
|
30
|
+
skillDir?: string;
|
|
31
|
+
knowledgeSource?: KnowledgeSource;
|
|
32
|
+
signalSource?: SignalSource;
|
|
33
|
+
dreamStore?: DreamStore;
|
|
34
|
+
agentId?: string;
|
|
35
|
+
governance?: Governance;
|
|
36
|
+
/** Host-provided skill content map (name → markdown body). WASM has no fs access. */
|
|
37
|
+
skillContentMap?: Map<string, string>;
|
|
38
|
+
}
|
|
39
|
+
export declare class Agent {
|
|
40
|
+
private readonly provider;
|
|
41
|
+
private readonly options;
|
|
42
|
+
private tools;
|
|
43
|
+
private blockedTools;
|
|
44
|
+
private extensions;
|
|
45
|
+
private interrupted;
|
|
46
|
+
private pendingInterrupt;
|
|
47
|
+
private _pendingSkills;
|
|
48
|
+
constructor(provider: LLMProvider, options: AgentOptions);
|
|
49
|
+
register(...tools: RegisteredTool[]): this;
|
|
50
|
+
unregister(name: string): this;
|
|
51
|
+
blockTool(name: string): this;
|
|
52
|
+
interrupt(): void;
|
|
53
|
+
run(goal: string, criteria?: string[], extensions?: Record<string, unknown>): Promise<string>;
|
|
54
|
+
runStreaming(goal: string, criteria?: string[], extensions?: Record<string, unknown>): AsyncIterable<StreamEvent>;
|
|
55
|
+
/** Register available skills from host-provided metadata (WASM has no fs access). */
|
|
56
|
+
setAvailableSkills(skills: SkillMetadata[]): void;
|
|
57
|
+
}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { executeTools } from "./tools/index.js";
|
|
2
|
+
async function loadKernel() {
|
|
3
|
+
return import("@deepstrike/wasm-kernel");
|
|
4
|
+
}
|
|
5
|
+
export class Agent {
|
|
6
|
+
provider;
|
|
7
|
+
options;
|
|
8
|
+
tools = new Map();
|
|
9
|
+
blockedTools = new Set();
|
|
10
|
+
extensions;
|
|
11
|
+
interrupted = false;
|
|
12
|
+
pendingInterrupt = false;
|
|
13
|
+
_pendingSkills = [];
|
|
14
|
+
constructor(provider, options) {
|
|
15
|
+
this.provider = provider;
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.extensions = options.extensions ?? {};
|
|
18
|
+
}
|
|
19
|
+
register(...tools) {
|
|
20
|
+
for (const t of tools)
|
|
21
|
+
this.tools.set(t.schema.name, t);
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
unregister(name) { this.tools.delete(name); return this; }
|
|
25
|
+
blockTool(name) { this.blockedTools.add(name); return this; }
|
|
26
|
+
interrupt() { this.interrupted = true; }
|
|
27
|
+
async run(goal, criteria, extensions) {
|
|
28
|
+
let text = "";
|
|
29
|
+
for await (const evt of this.runStreaming(goal, criteria, extensions)) {
|
|
30
|
+
if (evt.type === "text_delta")
|
|
31
|
+
text += evt.delta;
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
async *runStreaming(goal, criteria, extensions) {
|
|
36
|
+
this.interrupted = false;
|
|
37
|
+
this.pendingInterrupt = false;
|
|
38
|
+
if (this.options.knowledgeSource) {
|
|
39
|
+
await this.options.knowledgeSource.init();
|
|
40
|
+
}
|
|
41
|
+
const kernel = await loadKernel();
|
|
42
|
+
if (this.options.governance)
|
|
43
|
+
this.options.governance._attach(kernel);
|
|
44
|
+
const ext = { ...this.extensions, ...(extensions ?? {}) };
|
|
45
|
+
const sm = new kernel.LoopStateMachine({
|
|
46
|
+
maxTokens: this.options.maxTokens,
|
|
47
|
+
maxTurns: this.options.maxTurns ?? 25,
|
|
48
|
+
timeoutMs: this.options.timeoutMs,
|
|
49
|
+
});
|
|
50
|
+
sm.setTools(Array.from(this.tools.values()).map(t => t.schema));
|
|
51
|
+
if (this.options.systemPrompt) {
|
|
52
|
+
const tokens = Math.max(1, Math.ceil(this.options.systemPrompt.length / 4));
|
|
53
|
+
sm.addSystemMessage(this.options.systemPrompt, tokens);
|
|
54
|
+
}
|
|
55
|
+
for (const mem of this.options.initialMemory ?? []) {
|
|
56
|
+
sm.addMemoryMessage(mem, Math.max(1, Math.ceil(mem.length / 4)));
|
|
57
|
+
}
|
|
58
|
+
if (this._pendingSkills.length > 0) {
|
|
59
|
+
sm.setAvailableSkills(this._pendingSkills);
|
|
60
|
+
}
|
|
61
|
+
if (this.options.dreamStore && this.options.agentId)
|
|
62
|
+
sm.setMemoryEnabled(true);
|
|
63
|
+
if (this.options.knowledgeSource)
|
|
64
|
+
sm.setKnowledgeEnabled(true);
|
|
65
|
+
const router = new kernel.SignalRouter(256);
|
|
66
|
+
let action = sm.start({ goal, criteria: criteria ?? [] });
|
|
67
|
+
let finalText = "";
|
|
68
|
+
const sessionStart = Date.now();
|
|
69
|
+
const sessionMsgs = [{ role: "user", content: goal }];
|
|
70
|
+
while (!sm.isTerminal()) {
|
|
71
|
+
if (this.interrupted) {
|
|
72
|
+
action = sm.feedTimeout();
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (this.pendingInterrupt) {
|
|
76
|
+
this.pendingInterrupt = false;
|
|
77
|
+
action = sm.feedTimeout();
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (this.options.signalSource) {
|
|
81
|
+
const sig = await this.options.signalSource.nextSignal();
|
|
82
|
+
if (sig) {
|
|
83
|
+
const kernelSig = {
|
|
84
|
+
id: crypto.randomUUID(),
|
|
85
|
+
source: sig.source,
|
|
86
|
+
signalType: sig.signalType,
|
|
87
|
+
urgency: sig.urgency,
|
|
88
|
+
summary: String(sig.payload?.goal ?? sig.signalType),
|
|
89
|
+
payload: JSON.stringify(sig.payload ?? {}),
|
|
90
|
+
dedupeKey: sig.dedupeKey,
|
|
91
|
+
timestampMs: Date.now(),
|
|
92
|
+
};
|
|
93
|
+
const disposition = router.ingest(kernelSig, action.kind === "execute_tools");
|
|
94
|
+
if (disposition === "interrupt_now") {
|
|
95
|
+
action = sm.feedTimeout();
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (disposition === "interrupt")
|
|
99
|
+
this.pendingInterrupt = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
sm.takeObservations();
|
|
103
|
+
if (action.kind === "call_llm") {
|
|
104
|
+
finalText = "";
|
|
105
|
+
const finalToolCalls = [];
|
|
106
|
+
const messages = (action.messages ?? []);
|
|
107
|
+
const tools = (action.tools ?? []);
|
|
108
|
+
try {
|
|
109
|
+
for await (const evt of this.provider.stream(messages, tools, Object.keys(ext).length ? ext : undefined)) {
|
|
110
|
+
yield evt;
|
|
111
|
+
if (evt.type === "text_delta")
|
|
112
|
+
finalText += evt.delta;
|
|
113
|
+
else if (evt.type === "tool_call") {
|
|
114
|
+
const tc = evt;
|
|
115
|
+
finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
yield { type: "error", message: String(err) };
|
|
121
|
+
action = sm.feedTimeout();
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
action = sm.feedLlmResponse({ role: "assistant", content: finalText, toolCalls: finalToolCalls });
|
|
125
|
+
sessionMsgs.push({ role: "assistant", content: finalText, toolCalls: finalToolCalls });
|
|
126
|
+
}
|
|
127
|
+
else if (action.kind === "execute_tools") {
|
|
128
|
+
const allCalls = (action.calls ?? []);
|
|
129
|
+
// Governance check
|
|
130
|
+
const permittedCalls = [];
|
|
131
|
+
for (const c of allCalls) {
|
|
132
|
+
if (this.blockedTools.has(c.name)) {
|
|
133
|
+
yield { type: "error", message: `tool blocked: ${c.name}` };
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (this.options.governance) {
|
|
137
|
+
this.options.governance.setTime(Date.now());
|
|
138
|
+
const verdict = this.options.governance.evaluate(c.name, c.arguments);
|
|
139
|
+
if (verdict.kind === "deny") {
|
|
140
|
+
yield { type: "error", message: `permission denied: ${c.name} — ${verdict.reason}` };
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (verdict.kind === "ask_user") {
|
|
144
|
+
yield { type: "permission_request", callId: c.id, toolName: c.name, arguments: c.arguments, reason: verdict.reason };
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
permittedCalls.push(c);
|
|
149
|
+
}
|
|
150
|
+
const skillCalls = permittedCalls.filter(c => c.name === "skill");
|
|
151
|
+
const memoryCalls = permittedCalls.filter(c => c.name === "memory");
|
|
152
|
+
const knowledgeCalls = permittedCalls.filter(c => c.name === "knowledge");
|
|
153
|
+
const regularCalls = permittedCalls.filter(c => !["skill", "memory", "knowledge"].includes(c.name));
|
|
154
|
+
// skill: WASM host must provide skill content via a registered tool or extension
|
|
155
|
+
const skillContentMap = this.options.skillContentMap ?? new Map();
|
|
156
|
+
const skillResults = skillCalls.map(c => {
|
|
157
|
+
const args = tryParseJson(c.arguments);
|
|
158
|
+
const name = String(args?.name ?? "");
|
|
159
|
+
const content = skillContentMap.get(name);
|
|
160
|
+
const output = content ?? `Skill "${name}" not found.`;
|
|
161
|
+
return { callId: c.id, output, isError: content === undefined };
|
|
162
|
+
});
|
|
163
|
+
const memoryResults = [];
|
|
164
|
+
if (this.options.dreamStore && this.options.agentId) {
|
|
165
|
+
for (const c of memoryCalls) {
|
|
166
|
+
const args = tryParseJson(c.arguments);
|
|
167
|
+
const query = String(args?.query ?? "");
|
|
168
|
+
const topK = typeof args?.top_k === "number" ? args.top_k : 5;
|
|
169
|
+
const entries = await this.options.dreamStore.search(this.options.agentId, query, topK);
|
|
170
|
+
const output = entries.length ? entries.map(e => `[score=${e.score.toFixed(3)}] ${e.text}`).join("\n---\n") : "No relevant memories found.";
|
|
171
|
+
yield { type: "tool_result", callId: c.id, name: c.name, content: output, isError: false };
|
|
172
|
+
memoryResults.push({ callId: c.id, output, isError: false });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
for (const c of memoryCalls)
|
|
177
|
+
memoryResults.push({ callId: c.id, output: "Memory retrieval not configured.", isError: true });
|
|
178
|
+
}
|
|
179
|
+
const knowledgeResults = [];
|
|
180
|
+
if (this.options.knowledgeSource) {
|
|
181
|
+
for (const c of knowledgeCalls) {
|
|
182
|
+
const args = tryParseJson(c.arguments);
|
|
183
|
+
const query = String(args?.query ?? "");
|
|
184
|
+
const topK = typeof args?.top_k === "number" ? args.top_k : 5;
|
|
185
|
+
const snippets = await this.options.knowledgeSource.retrieve(query, topK);
|
|
186
|
+
const output = snippets.length ? snippets.join("\n---\n") : "No relevant knowledge found.";
|
|
187
|
+
yield { type: "tool_result", callId: c.id, name: c.name, content: output, isError: false };
|
|
188
|
+
knowledgeResults.push({ callId: c.id, output, isError: false });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
for (const c of knowledgeCalls)
|
|
193
|
+
knowledgeResults.push({ callId: c.id, output: "Knowledge source not configured.", isError: true });
|
|
194
|
+
}
|
|
195
|
+
const results = await executeTools(regularCalls, this.tools);
|
|
196
|
+
for (const r of results) {
|
|
197
|
+
const name = regularCalls.find(c => c.id === r.callId)?.name ?? "";
|
|
198
|
+
yield { type: "tool_result", callId: r.callId, name, content: r.output, isError: r.isError };
|
|
199
|
+
}
|
|
200
|
+
action = sm.feedToolResults([
|
|
201
|
+
...skillResults,
|
|
202
|
+
...memoryResults,
|
|
203
|
+
...knowledgeResults,
|
|
204
|
+
...results.map(r => ({ callId: r.callId, output: r.output, isError: r.isError })),
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
else if (action.kind === "done") {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const result = action.result;
|
|
212
|
+
if (this.options.dreamStore && this.options.agentId && sessionMsgs.length > 1) {
|
|
213
|
+
try {
|
|
214
|
+
await this.options.dreamStore.saveSession({
|
|
215
|
+
sessionId: crypto.randomUUID(),
|
|
216
|
+
agentId: this.options.agentId,
|
|
217
|
+
messages: sessionMsgs,
|
|
218
|
+
metadata: null,
|
|
219
|
+
createdAtMs: sessionStart,
|
|
220
|
+
updatedAtMs: Date.now(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch { /* session save failure must not surface to caller */ }
|
|
224
|
+
}
|
|
225
|
+
yield {
|
|
226
|
+
type: "done",
|
|
227
|
+
iterations: result?.turnsUsed ?? 0,
|
|
228
|
+
totalTokens: Number(result?.totalTokensUsed ?? 0),
|
|
229
|
+
status: result?.termination ?? "error",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Register available skills from host-provided metadata (WASM has no fs access). */
|
|
233
|
+
setAvailableSkills(skills) {
|
|
234
|
+
this._pendingSkills = skills;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function tryParseJson(s) {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(s);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface GovernanceVerdict {
|
|
2
|
+
kind: "allow" | "deny" | "rate_limited" | "ask_user";
|
|
3
|
+
reason?: string;
|
|
4
|
+
retryAfterMs?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class Governance {
|
|
7
|
+
private _inner;
|
|
8
|
+
private _pendingBlocks;
|
|
9
|
+
/** Called by Agent after the WASM kernel module is loaded. */
|
|
10
|
+
_attach(kernel: typeof import("@deepstrike/wasm-kernel")): void;
|
|
11
|
+
blockTool(name: string): this;
|
|
12
|
+
setTime(nowMs: number): this;
|
|
13
|
+
evaluate(toolName: string, argsJson: string): GovernanceVerdict;
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class Governance {
|
|
2
|
+
_inner = null;
|
|
3
|
+
_pendingBlocks = [];
|
|
4
|
+
/** Called by Agent after the WASM kernel module is loaded. */
|
|
5
|
+
_attach(kernel) {
|
|
6
|
+
if (this._inner)
|
|
7
|
+
return;
|
|
8
|
+
this._inner = new kernel.Governance();
|
|
9
|
+
for (const name of this._pendingBlocks)
|
|
10
|
+
this._inner.blockTool(name);
|
|
11
|
+
}
|
|
12
|
+
blockTool(name) {
|
|
13
|
+
if (this._inner)
|
|
14
|
+
this._inner.blockTool(name);
|
|
15
|
+
else
|
|
16
|
+
this._pendingBlocks.push(name);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
setTime(nowMs) {
|
|
20
|
+
this._inner?.setTime(nowMs);
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
evaluate(toolName, argsJson) {
|
|
24
|
+
if (!this._inner)
|
|
25
|
+
return { kind: "allow" };
|
|
26
|
+
return this._inner.evaluate(toolName, argsJson);
|
|
27
|
+
}
|
|
28
|
+
}
|