@geravant/sinain 1.10.0 → 1.11.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/package.json +1 -1
- package/sinain-agent/CLAUDE.md +50 -0
- package/sinain-agent/run.sh +73 -10
- package/sinain-core/src/agent/analyzer.ts +4 -27
- package/sinain-core/src/agent/loop.ts +10 -40
- package/sinain-core/src/agent/situation-writer.ts +0 -16
- package/sinain-core/src/config.ts +1 -9
- package/sinain-core/src/escalation/escalator.ts +43 -16
- package/sinain-core/src/index.ts +316 -61
- package/sinain-core/src/learning/local-curation.ts +373 -0
- package/sinain-core/src/overlay/commands.ts +31 -11
- package/sinain-core/src/overlay/ws-handler.ts +10 -1
- package/sinain-core/src/server.ts +318 -0
- package/sinain-core/src/types.ts +22 -28
- package/sinain-mcp-server/index.ts +62 -4
- package/sinain-memory/eval/assertions.py +0 -21
- package/sinain-memory/eval/retrieval_benchmark.jsonl +12 -0
- package/sinain-memory/eval/retrieval_evaluator.py +186 -0
- package/sinain-memory/graph_query.py +34 -1
- package/sinain-memory/knowledge_integrator.py +54 -0
- package/sinain-memory/triplestore.py +76 -5
- package/sinain-core/src/agent/traits.ts +0 -520
package/package.json
CHANGED
package/sinain-agent/CLAUDE.md
CHANGED
|
@@ -55,6 +55,56 @@ When responding to escalations:
|
|
|
55
55
|
4. Optionally call `sinain_get_knowledge` to review the portable knowledge document
|
|
56
56
|
5. Optionally call `sinain_get_feedback` to review recent escalation scores
|
|
57
57
|
|
|
58
|
+
## Knowledge System
|
|
59
|
+
|
|
60
|
+
Knowledge is stored in a **dual-database** architecture with two SQLite triplestore databases:
|
|
61
|
+
|
|
62
|
+
| Database | Path | Written by |
|
|
63
|
+
|----------|------|------------|
|
|
64
|
+
| **Local** | `~/.sinain/memory/knowledge-graph.db` | `LocalCurationService` (session distillation on shutdown, periodic curation every 30 min) |
|
|
65
|
+
| **Workspace** | `~/.openclaw/workspace/memory/knowledge-graph.db` | Heartbeat curation scripts (`sinain_heartbeat_tick`) |
|
|
66
|
+
|
|
67
|
+
### Knowledge Tools
|
|
68
|
+
|
|
69
|
+
| Tool | What it does |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `sinain_get_knowledge` | Read the portable knowledge document (playbook + top facts) from workspace |
|
|
72
|
+
| `sinain_knowledge_query` | Query facts by entity/keyword — queries **both** DBs via sinain-core API |
|
|
73
|
+
| `sinain_distill_session` | Explicitly distill current session into knowledge updates |
|
|
74
|
+
|
|
75
|
+
### HTTP Knowledge API (sinain-core, port 9500)
|
|
76
|
+
|
|
77
|
+
These endpoints query **both** databases and merge results:
|
|
78
|
+
|
|
79
|
+
| Endpoint | Purpose |
|
|
80
|
+
|----------|---------|
|
|
81
|
+
| `GET /knowledge` | Portable knowledge document |
|
|
82
|
+
| `GET /knowledge/facts?entities=X&max=N` | Query facts by keyword tags |
|
|
83
|
+
| `GET /knowledge/entities?max=N` | List all entities with attributes |
|
|
84
|
+
| `GET /knowledge/export?domain=X&max=N` | Export facts as portable JSON |
|
|
85
|
+
| `POST /knowledge/import` | Import facts (deduplicates automatically) |
|
|
86
|
+
| `GET /knowledge/ui` | Web UI for browsing/managing knowledge |
|
|
87
|
+
|
|
88
|
+
### How Knowledge Flows
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Session (screen + audio) → LocalCurationService → Local DB
|
|
92
|
+
↓ (queried together)
|
|
93
|
+
Heartbeat tick → curation scripts ──────────→ Workspace DB
|
|
94
|
+
↓
|
|
95
|
+
Knowledge API (localhost:9500) ← merges both DBs ← queries
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- **Local DB** gets real-time session knowledge (audio transcripts, screen patterns, German lessons, etc.)
|
|
99
|
+
- **Workspace DB** gets heartbeat-curated knowledge (playbook patterns, feedback analysis)
|
|
100
|
+
- The Knowledge API merges both — use `sinain_knowledge_query` for combined results
|
|
101
|
+
- Facts have confidence decay (60-day half-life) — reinforcement resets the clock
|
|
102
|
+
- Export/import via `/knowledge/export` → `/knowledge/import` enables cross-instance transfer
|
|
103
|
+
|
|
104
|
+
### Using Knowledge in Escalation Responses
|
|
105
|
+
|
|
106
|
+
When responding to escalations, call `sinain_knowledge_query` with relevant entities to enrich your response with long-term knowledge. Example: if the user is working on German grammar, query `sinain_knowledge_query(entities=["german", "grammar"])` to retrieve previously learned patterns.
|
|
107
|
+
|
|
58
108
|
## Spawning Background Tasks
|
|
59
109
|
|
|
60
110
|
When an escalation suggests deeper research would help:
|
package/sinain-agent/run.sh
CHANGED
|
@@ -69,11 +69,22 @@ invoke_agent() {
|
|
|
69
69
|
case "$AGENT" in
|
|
70
70
|
claude)
|
|
71
71
|
local turns="${2:-$AGENT_MAX_TURNS}"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
if [ -n "${SINAIN_SPAWN:-}" ]; then
|
|
73
|
+
# Spawn: PreToolUse hook routes permission prompts to overlay HUD
|
|
74
|
+
claude \
|
|
75
|
+
--mcp-config "$MCP_CONFIG" \
|
|
76
|
+
--settings "$SCRIPT_DIR/.claude/settings.json" \
|
|
77
|
+
${ALLOWED_TOOLS:+--allowedTools $ALLOWED_TOOLS} \
|
|
78
|
+
--max-turns "$turns" --output-format text \
|
|
79
|
+
-p "$prompt"
|
|
80
|
+
else
|
|
81
|
+
# Escalation: auto-approve for speed (short-lived, read-heavy)
|
|
82
|
+
claude --enable-auto-mode \
|
|
83
|
+
--mcp-config "$MCP_CONFIG" \
|
|
84
|
+
${ALLOWED_TOOLS:+--allowedTools $ALLOWED_TOOLS} \
|
|
85
|
+
--max-turns "$turns" --output-format text \
|
|
86
|
+
-p "$prompt"
|
|
87
|
+
fi
|
|
77
88
|
;;
|
|
78
89
|
codex)
|
|
79
90
|
codex exec -s danger-full-access \
|
|
@@ -97,6 +108,8 @@ invoke_agent() {
|
|
|
97
108
|
local turns="${2:-$AGENT_MAX_TURNS}"
|
|
98
109
|
GOOSE_MODE=auto goose run --text "$prompt" \
|
|
99
110
|
--output-format text \
|
|
111
|
+
--quiet \
|
|
112
|
+
--no-session \
|
|
100
113
|
--max-turns "$turns"
|
|
101
114
|
;;
|
|
102
115
|
aider)
|
|
@@ -186,6 +199,34 @@ if [ "$AGENT" = "codex" ]; then
|
|
|
186
199
|
fi
|
|
187
200
|
fi
|
|
188
201
|
|
|
202
|
+
# Goose: auto-register sinain MCP server in config.yaml if not present
|
|
203
|
+
if [ "$AGENT" = "goose" ]; then
|
|
204
|
+
TSX_BIN="$(cd "$SCRIPT_DIR/.." && pwd)/sinain-core/node_modules/.bin/tsx"
|
|
205
|
+
MCP_ENTRY="$(cd "$SCRIPT_DIR/.." && pwd)/sinain-mcp-server/index.ts"
|
|
206
|
+
GOOSE_CONFIG="${HOME}/.config/goose/config.yaml"
|
|
207
|
+
if [ -f "$GOOSE_CONFIG" ] && ! grep -q "sinain:" "$GOOSE_CONFIG" 2>/dev/null; then
|
|
208
|
+
echo "Registering sinain MCP server with goose..."
|
|
209
|
+
python3 -c "
|
|
210
|
+
import yaml, os, sys
|
|
211
|
+
config_path = sys.argv[1]
|
|
212
|
+
with open(config_path) as f:
|
|
213
|
+
cfg = yaml.safe_load(f) or {}
|
|
214
|
+
cfg.setdefault('extensions', {})['sinain'] = {
|
|
215
|
+
'name': 'Sinain MCP Server',
|
|
216
|
+
'cmd': sys.argv[2],
|
|
217
|
+
'args': [sys.argv[3]],
|
|
218
|
+
'enabled': True,
|
|
219
|
+
'envs': {'SINAIN_CORE_URL': sys.argv[4], 'SINAIN_WORKSPACE': sys.argv[5]},
|
|
220
|
+
'type': 'stdio',
|
|
221
|
+
'timeout': 300,
|
|
222
|
+
}
|
|
223
|
+
with open(config_path, 'w') as f:
|
|
224
|
+
yaml.dump(cfg, f, default_flow_style=False)
|
|
225
|
+
print(' sinain extension added to ' + config_path)
|
|
226
|
+
" "$GOOSE_CONFIG" "$TSX_BIN" "$MCP_ENTRY" "$CORE_URL" "$WORKSPACE"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
|
|
189
230
|
# Agent mode label
|
|
190
231
|
if agent_has_mcp; then
|
|
191
232
|
AGENT_MODE="MCP"
|
|
@@ -221,15 +262,24 @@ Call sinain_get_escalation to see the full context, then call sinain_respond wit
|
|
|
221
262
|
Response guidelines: 5-10 sentences, address errors first, reference specific screen/audio context, never NO_REPLY. Max 4000 chars for coding context, 3000 otherwise.'
|
|
222
263
|
|
|
223
264
|
HEARTBEAT_PROMPT='You are the sinain HUD agent. Run the heartbeat cycle:
|
|
224
|
-
1. Call sinain_heartbeat_tick with a brief session summary
|
|
225
|
-
2. If the result contains a suggestion, post it to HUD via sinain_post_feed
|
|
226
|
-
3. Call
|
|
265
|
+
1. Call sinain_heartbeat_tick with a brief session summary (runs signal analysis, session distillation, knowledge integration, insight synthesis)
|
|
266
|
+
2. If the result contains a suggestion or insight, post it to HUD via sinain_post_feed
|
|
267
|
+
3. Call sinain_get_knowledge to review the merged knowledge document (draws from both local and workspace databases)
|
|
268
|
+
4. Optionally call sinain_knowledge_query with relevant entities to check long-term knowledge state
|
|
269
|
+
5. Call sinain_get_feedback to review recent escalation scores
|
|
270
|
+
|
|
271
|
+
Knowledge context: sinain-core maintains two knowledge databases — local (session distillation) and workspace (heartbeat curation). The knowledge tools query both via the sinain-core API. Facts have confidence decay (60-day half-life).'
|
|
227
272
|
|
|
228
273
|
# --- Main loop ---
|
|
229
274
|
|
|
230
275
|
while true; do
|
|
231
276
|
# Poll for pending escalation
|
|
232
277
|
ESC=$(curl -sf "$CORE_URL/escalation/pending" 2>/dev/null || echo '{"ok":false}')
|
|
278
|
+
ESC_PAUSED=$(echo "$ESC" | python3 -c "import sys,json; d=json.load(sys.stdin); print('true' if d.get('paused') else '')" 2>/dev/null || true)
|
|
279
|
+
if [ -n "$ESC_PAUSED" ]; then
|
|
280
|
+
sleep 10 # Slow polling when paused
|
|
281
|
+
continue
|
|
282
|
+
fi
|
|
233
283
|
ESC_ID=$(echo "$ESC" | python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('escalation'); print(e['id'] if e else '')" 2>/dev/null || true)
|
|
234
284
|
|
|
235
285
|
if [ -n "$ESC_ID" ]; then
|
|
@@ -270,10 +320,23 @@ while true; do
|
|
|
270
320
|
|
|
271
321
|
if agent_has_mcp; then
|
|
272
322
|
# MCP path: agent runs task with sinain tools available
|
|
323
|
+
# Pre-fetch knowledge context so the spawn doesn't waste turns calling tools
|
|
324
|
+
SPAWN_KNOWLEDGE=$(curl -sf "$CORE_URL/knowledge" 2>/dev/null | python3 -c "
|
|
325
|
+
import sys, json
|
|
326
|
+
d = json.load(sys.stdin)
|
|
327
|
+
k = d.get('knowledge', '')
|
|
328
|
+
# Trim to 2000 chars to avoid prompt bloat
|
|
329
|
+
print(k[:2000])
|
|
330
|
+
" 2>/dev/null || true)
|
|
273
331
|
SPAWN_PROMPT="You have a background task to complete. Task: $SPAWN_TASK
|
|
274
|
-
|
|
275
|
-
|
|
332
|
+
${SPAWN_KNOWLEDGE:+
|
|
333
|
+
## Knowledge Context
|
|
334
|
+
$SPAWN_KNOWLEDGE
|
|
335
|
+
}
|
|
336
|
+
Complete this task thoroughly. You also have sinain_get_knowledge and sinain_knowledge_query tools available for additional context. Summarize your findings concisely."
|
|
337
|
+
export SINAIN_SPAWN=1 SINAIN_SPAWN_TASK_ID="$SPAWN_ID"
|
|
276
338
|
SPAWN_RESULT=$(invoke_agent "$SPAWN_PROMPT" "$SPAWN_MAX_TURNS" || echo "ERROR: agent invocation failed")
|
|
339
|
+
unset SINAIN_SPAWN SINAIN_SPAWN_TASK_ID
|
|
277
340
|
else
|
|
278
341
|
# Pipe path: agent gets task text directly
|
|
279
342
|
SPAWN_RESULT=$(invoke_pipe "Background task: $SPAWN_TASK" || echo "No output")
|
|
@@ -53,13 +53,12 @@ You produce outputs as JSON.
|
|
|
53
53
|
Respond ONLY with valid JSON. No markdown, no code fences, no explanation.
|
|
54
54
|
Your entire response must be parseable by JSON.parse().
|
|
55
55
|
|
|
56
|
-
{"hud":"...","digest":"...","record":{"command":"start"|"stop","label":"..."}
|
|
56
|
+
{"hud":"...","digest":"...","record":{"command":"start"|"stop","label":"..."}}
|
|
57
57
|
|
|
58
58
|
Output fields:
|
|
59
59
|
- "hud" (required): max 60 words describing what user is doing NOW
|
|
60
60
|
- "digest" (required): 5-8 sentences with detailed activity description
|
|
61
61
|
- "record" (optional): control recording — {"command":"start","label":"Meeting name"} or {"command":"stop"}
|
|
62
|
-
- "task" (optional): natural language instruction to spawn a background task
|
|
63
62
|
|
|
64
63
|
When to use "record":
|
|
65
64
|
- START when user begins a meeting, call, lecture, YouTube video, or important audio content
|
|
@@ -67,25 +66,6 @@ When to use "record":
|
|
|
67
66
|
- Provide descriptive labels like "Team standup", "Client call", "YouTube: [video title from OCR]"
|
|
68
67
|
- For YouTube/video content: extract video title from screen OCR for the label
|
|
69
68
|
|
|
70
|
-
When to use "task":
|
|
71
|
-
- User explicitly asks for research, lookup, or action
|
|
72
|
-
- Something needs external search or processing that isn't a real-time response
|
|
73
|
-
- Example: "Search for React 19 migration guide", "Find docs for this API"
|
|
74
|
-
|
|
75
|
-
When to spawn "task" for video content:
|
|
76
|
-
- If user watches a YouTube video for 2+ minutes AND no task has been spawned for this video yet, spawn: "Summarize YouTube video: [title or URL from OCR]"
|
|
77
|
-
- ONLY spawn ONCE per video - do not repeat spawn for the same video in subsequent ticks
|
|
78
|
-
- Extract video title or URL from screen OCR to include in the task
|
|
79
|
-
|
|
80
|
-
When to spawn "task" for coding problems:
|
|
81
|
-
- If user is actively working on a coding problem/challenge for 1+ minutes:
|
|
82
|
-
- Spawn: "Solve coding problem: [problem description/title from OCR]"
|
|
83
|
-
- This includes LeetCode, HackerRank, interviews, coding assessments, or any visible coding challenge
|
|
84
|
-
- Look for problem signals: "Input:", "Output:", "Example", "Constraints:", problem titles, test cases
|
|
85
|
-
- Include as much context as possible from the screen OCR (problem description, examples, constraints)
|
|
86
|
-
- ONLY spawn ONCE per distinct problem - do not repeat for the same problem
|
|
87
|
-
- The spawned task should provide a complete solution with code and explanation
|
|
88
|
-
|
|
89
69
|
Audio sources: [\ud83d\udd0a]=system/speaker audio, [\ud83c\udf99]=microphone (user's voice).
|
|
90
70
|
Treat [\ud83c\udf99] as direct user speech. Treat [\ud83d\udd0a] as external audio.
|
|
91
71
|
|
|
@@ -95,7 +75,7 @@ Rules:
|
|
|
95
75
|
- If nothing is happening, hud="Idle" and digest explains what was last seen.
|
|
96
76
|
- Include specific filenames, URLs, error messages, UI text from OCR in digest.
|
|
97
77
|
- Do NOT suggest actions in digest — just describe the situation factually.
|
|
98
|
-
- Only include "record"
|
|
78
|
+
- Only include "record" when genuinely appropriate — most responses won't have it.
|
|
99
79
|
- CRITICAL: Output ONLY the JSON object, nothing else.`;
|
|
100
80
|
|
|
101
81
|
/**
|
|
@@ -210,7 +190,6 @@ export async function analyzeContext(
|
|
|
210
190
|
contextWindow: ContextWindow,
|
|
211
191
|
config: AnalysisConfig,
|
|
212
192
|
recorderStatus: RecorderStatus | null = null,
|
|
213
|
-
traitSystemPrompt?: string,
|
|
214
193
|
): Promise<AgentResult> {
|
|
215
194
|
const userPrompt = buildUserPrompt(contextWindow, recorderStatus);
|
|
216
195
|
|
|
@@ -221,10 +200,8 @@ export async function analyzeContext(
|
|
|
221
200
|
if (levelFor("screen_images", privacyDest) === "none") images = [];
|
|
222
201
|
} catch { /* privacy not initialized, keep images */ }
|
|
223
202
|
|
|
224
|
-
const systemPrompt = traitSystemPrompt ?? SYSTEM_PROMPT;
|
|
225
|
-
|
|
226
203
|
if (config.provider === "ollama") {
|
|
227
|
-
return await callOllama(
|
|
204
|
+
return await callOllama(SYSTEM_PROMPT, userPrompt, images, config);
|
|
228
205
|
}
|
|
229
206
|
|
|
230
207
|
// OpenRouter path: model chain with fallbacks
|
|
@@ -241,7 +218,7 @@ export async function analyzeContext(
|
|
|
241
218
|
let lastError: Error | null = null;
|
|
242
219
|
for (const model of models) {
|
|
243
220
|
try {
|
|
244
|
-
return await callOpenRouter(
|
|
221
|
+
return await callOpenRouter(SYSTEM_PROMPT, userPrompt, images, model, config);
|
|
245
222
|
} catch (err: any) {
|
|
246
223
|
lastError = err;
|
|
247
224
|
log(TAG, `model ${model} failed: ${err.message || err}, trying next...`);
|
|
@@ -10,8 +10,6 @@ import { analyzeContext } from "./analyzer.js";
|
|
|
10
10
|
import { writeSituationMd } from "./situation-writer.js";
|
|
11
11
|
import { calculateEscalationScore } from "../escalation/scorer.js";
|
|
12
12
|
import { log, warn, error, debug } from "../log.js";
|
|
13
|
-
import type { TraitEngine, TraitSelection } from "./traits.js";
|
|
14
|
-
import { writeTraitLog } from "./traits.js";
|
|
15
13
|
|
|
16
14
|
const TAG = "agent";
|
|
17
15
|
|
|
@@ -33,10 +31,6 @@ export interface AgentLoopDeps {
|
|
|
33
31
|
profiler?: Profiler;
|
|
34
32
|
/** Called after each successful SITUATION.md write with the content string. */
|
|
35
33
|
onSituationUpdate?: (content: string) => void;
|
|
36
|
-
/** Optional trait engine for personality voice selection. */
|
|
37
|
-
traitEngine?: TraitEngine;
|
|
38
|
-
/** Directory to write per-day trait log JSONL files. */
|
|
39
|
-
traitLogDir?: string;
|
|
40
34
|
/** Optional: path to sinain-knowledge.md for startup recap. */
|
|
41
35
|
getKnowledgeDocPath?: () => string | null;
|
|
42
36
|
/** Optional: feedback store for startup recap context. */
|
|
@@ -283,14 +277,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
283
277
|
traceCtx?.startSpan("llm-call");
|
|
284
278
|
const recorderStatus = this.deps.getRecorderStatus?.() ?? null;
|
|
285
279
|
|
|
286
|
-
// Trait selection: pick the best personality voice for this tick
|
|
287
|
-
let traitSelection: TraitSelection | null = null;
|
|
288
|
-
if (this.deps.traitEngine?.enabled) {
|
|
289
|
-
const ocrText = contextWindow.screen.map(e => e.ocr ?? "").join(" ");
|
|
290
|
-
const audioText = contextWindow.audio.map(e => e.text).join(" ");
|
|
291
|
-
traitSelection = this.deps.traitEngine.selectTrait(ocrText, audioText);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
280
|
const result = await analyzeContext(contextWindow, this.deps.agentConfig, recorderStatus);
|
|
295
281
|
this.deps.profiler?.timerRecord("agent.llmCall", result.latencyMs);
|
|
296
282
|
traceCtx?.endSpan({ model: result.model, tokensIn: result.tokensIn, tokensOut: result.tokensOut, latencyMs: result.latencyMs });
|
|
@@ -346,11 +332,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
346
332
|
screenCount: contextWindow.screenCount,
|
|
347
333
|
},
|
|
348
334
|
};
|
|
349
|
-
if (traitSelection) {
|
|
350
|
-
entry.voice = traitSelection.trait.name;
|
|
351
|
-
entry.voice_stat = traitSelection.stat;
|
|
352
|
-
entry.voice_confidence = traitSelection.confidence;
|
|
353
|
-
}
|
|
354
335
|
this.agentBuffer.push(entry);
|
|
355
336
|
const historyLimit = this.deps.agentConfig.historyLimit || 50;
|
|
356
337
|
if (this.agentBuffer.length > historyLimit) this.agentBuffer.shift();
|
|
@@ -362,17 +343,22 @@ export class AgentLoop extends EventEmitter {
|
|
|
362
343
|
debug(TAG, `#${entry.id} (${latencyMs}ms) hud unchanged`);
|
|
363
344
|
}
|
|
364
345
|
|
|
365
|
-
//
|
|
346
|
+
// Always push HUD to feed buffer for data capture (curation pipeline reads this)
|
|
366
347
|
if (this.deps.agentConfig.pushToFeed &&
|
|
367
|
-
this.deps.escalationMode !== "focus" &&
|
|
368
|
-
this.deps.escalationMode !== "rich" &&
|
|
369
348
|
hud !== "\u2014" && hud !== "Idle" && hud !== this.lastPushedHud) {
|
|
370
349
|
feedBuffer.push(`[\ud83e\udde0] ${hud}`, "normal", "agent", "stream");
|
|
371
|
-
this.deps.onHudUpdate(`[\ud83e\udde0] ${hud}`);
|
|
372
350
|
this.lastPushedHud = hud;
|
|
373
351
|
entry.pushed = true;
|
|
374
352
|
}
|
|
375
353
|
|
|
354
|
+
// Broadcast to overlay only when NOT in focus/rich mode
|
|
355
|
+
// (in those modes, the overlay gets updates via escalation instead)
|
|
356
|
+
if (entry.pushed &&
|
|
357
|
+
this.deps.escalationMode !== "focus" &&
|
|
358
|
+
this.deps.escalationMode !== "rich") {
|
|
359
|
+
this.deps.onHudUpdate(`[\ud83e\udde0] ${hud}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
376
362
|
// Store digest
|
|
377
363
|
this.latestDigest = entry;
|
|
378
364
|
|
|
@@ -380,7 +366,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
380
366
|
const escalationScore = calculateEscalationScore(digest, contextWindow);
|
|
381
367
|
|
|
382
368
|
// Write SITUATION.md (enhanced with escalation context and recorder status)
|
|
383
|
-
const situationContent = writeSituationMd(this.deps.situationMdPath, contextWindow, digest, entry, escalationScore, recorderStatus
|
|
369
|
+
const situationContent = writeSituationMd(this.deps.situationMdPath, contextWindow, digest, entry, escalationScore, recorderStatus);
|
|
384
370
|
this.deps.onSituationUpdate?.(situationContent);
|
|
385
371
|
|
|
386
372
|
// Notify for escalation check
|
|
@@ -406,22 +392,6 @@ export class AgentLoop extends EventEmitter {
|
|
|
406
392
|
hudChanged: entry.pushed,
|
|
407
393
|
});
|
|
408
394
|
|
|
409
|
-
// Fire-and-forget trait log
|
|
410
|
-
if (this.deps.traitEngine?.enabled && this.deps.traitLogDir) {
|
|
411
|
-
writeTraitLog(this.deps.traitLogDir, {
|
|
412
|
-
ts: new Date().toISOString(),
|
|
413
|
-
tickId: entry.id,
|
|
414
|
-
enabled: true,
|
|
415
|
-
voice: traitSelection?.trait.name ?? "none",
|
|
416
|
-
voice_stat: traitSelection?.stat ?? 0,
|
|
417
|
-
voice_confidence: traitSelection?.confidence ?? 0,
|
|
418
|
-
activation_scores: traitSelection?.allScores ?? {},
|
|
419
|
-
context_app: contextWindow.currentApp,
|
|
420
|
-
hud_length: entry.hud.length,
|
|
421
|
-
synthesis: false,
|
|
422
|
-
}).catch(() => {});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
395
|
} catch (err: any) {
|
|
426
396
|
error(TAG, "tick error:", err.message || err);
|
|
427
397
|
traceCtx?.endSpan({ status: "error", error: err.message });
|
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ContextWindow, AgentEntry, RecorderStatus } from "../types.js";
|
|
4
4
|
import type { EscalationScore } from "../escalation/scorer.js";
|
|
5
|
-
import type { TraitSelection } from "./traits.js";
|
|
6
5
|
import { normalizeAppName } from "./context-window.js";
|
|
7
6
|
import { log, error } from "../log.js";
|
|
8
7
|
|
|
@@ -46,7 +45,6 @@ export function writeSituationMd(
|
|
|
46
45
|
entry: AgentEntry,
|
|
47
46
|
escalationScore?: EscalationScore,
|
|
48
47
|
recorderStatus?: RecorderStatus | null,
|
|
49
|
-
traitSelection?: TraitSelection | null,
|
|
50
48
|
): string {
|
|
51
49
|
const dir = path.dirname(situationMdPath);
|
|
52
50
|
const tmpPath = situationMdPath + ".tmp";
|
|
@@ -151,20 +149,6 @@ export function writeSituationMd(
|
|
|
151
149
|
lines.push("");
|
|
152
150
|
}
|
|
153
151
|
|
|
154
|
-
// Trait voice: frame downstream agent's perspective
|
|
155
|
-
if (traitSelection) {
|
|
156
|
-
const { trait, stat } = traitSelection;
|
|
157
|
-
const voiceFlavor = stat >= 7 ? trait.voice_high : stat <= 2 ? trait.voice_low : trait.description;
|
|
158
|
-
lines.push("## Active Voice");
|
|
159
|
-
lines.push("");
|
|
160
|
-
lines.push(`- Trait: ${trait.name} (stat ${stat}/10)`);
|
|
161
|
-
lines.push(`- Tagline: ${trait.tagline}`);
|
|
162
|
-
lines.push(`- Voice: "${voiceFlavor}"`);
|
|
163
|
-
lines.push("");
|
|
164
|
-
lines.push("> Frame your response through this lens naturally. Do not name the trait explicitly.");
|
|
165
|
-
lines.push("");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
152
|
lines.push("## Metadata");
|
|
169
153
|
lines.push("");
|
|
170
154
|
lines.push(`- Screen events in window: ${contextWindow.screenCount}`);
|
|
@@ -2,7 +2,7 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
2
2
|
import { resolve, dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import type { CoreConfig, AudioPipelineConfig, TranscriptionConfig, AnalysisConfig, EscalationConfig, OpenClawConfig, EscalationMode, EscalationTransport, LearningConfig,
|
|
5
|
+
import type { CoreConfig, AudioPipelineConfig, TranscriptionConfig, AnalysisConfig, EscalationConfig, OpenClawConfig, EscalationMode, EscalationTransport, LearningConfig, PrivacyConfig, PrivacyMatrix, PrivacyLevel, PrivacyRow } from "./types.js";
|
|
6
6
|
import { PRESETS } from "./privacy/presets.js";
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -234,13 +234,6 @@ export function loadConfig(): CoreConfig {
|
|
|
234
234
|
retentionDays: intEnv("FEEDBACK_RETENTION_DAYS", 30),
|
|
235
235
|
};
|
|
236
236
|
|
|
237
|
-
const traitConfig: TraitConfig = {
|
|
238
|
-
enabled: boolEnv("TRAITS_ENABLED", false),
|
|
239
|
-
configPath: resolvePath(env("TRAITS_CONFIG", "~/.sinain/traits.json")),
|
|
240
|
-
entropyHigh: boolEnv("TRAIT_ENTROPY_HIGH", false),
|
|
241
|
-
logDir: resolvePath(env("TRAIT_LOG_DIR", "~/.sinain-core/traits")),
|
|
242
|
-
};
|
|
243
|
-
|
|
244
237
|
const privacyConfig = loadPrivacyConfig();
|
|
245
238
|
|
|
246
239
|
return {
|
|
@@ -257,7 +250,6 @@ export function loadConfig(): CoreConfig {
|
|
|
257
250
|
traceDir: resolvePath(env("TRACE_DIR", resolve(sinainDataDir(), "traces"))),
|
|
258
251
|
costDisplayEnabled: boolEnv("COST_DISPLAY_ENABLED", false),
|
|
259
252
|
learningConfig,
|
|
260
|
-
traitConfig,
|
|
261
253
|
privacyConfig,
|
|
262
254
|
};
|
|
263
255
|
}
|
|
@@ -520,11 +520,11 @@ ${recentLines.join("\n")}`;
|
|
|
520
520
|
message: task,
|
|
521
521
|
sessionKey: childSessionKey,
|
|
522
522
|
lane: "subagent",
|
|
523
|
-
extraSystemPrompt: this.buildChildSystemPrompt(task, label),
|
|
523
|
+
extraSystemPrompt: await this.buildChildSystemPrompt(task, label),
|
|
524
524
|
deliver: false,
|
|
525
525
|
idempotencyKey: idemKey,
|
|
526
526
|
label: label || undefined,
|
|
527
|
-
},
|
|
527
|
+
}, 10 * 60_000, { expectFinal: true });
|
|
528
528
|
|
|
529
529
|
log(TAG, `spawn-task RPC response: ${JSON.stringify(result).slice(0, 500)}`);
|
|
530
530
|
this.stats.totalSpawnResponses++;
|
|
@@ -581,8 +581,44 @@ ${recentLines.join("\n")}`;
|
|
|
581
581
|
}
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
-
/** Build a focused system prompt for the child subagent. */
|
|
585
|
-
private buildChildSystemPrompt(task: string, label?: string): string {
|
|
584
|
+
/** Build a focused system prompt for the child subagent, enriched with knowledge. */
|
|
585
|
+
private async buildChildSystemPrompt(task: string, label?: string): Promise<string> {
|
|
586
|
+
// Fetch relevant knowledge facts (same enrichment as escalations)
|
|
587
|
+
let knowledgeSection = "";
|
|
588
|
+
if (this.deps.queryKnowledgeFacts) {
|
|
589
|
+
try {
|
|
590
|
+
const entities: string[] = [];
|
|
591
|
+
// Extract keywords from task text for entity matching
|
|
592
|
+
const techKeywords = [
|
|
593
|
+
"react-native", "react", "flutter", "swift", "kotlin", "python",
|
|
594
|
+
"typescript", "node", "docker", "sinain", "openclaw", "overlay",
|
|
595
|
+
"sense", "audio", "transcription", "escalation", "knowledge",
|
|
596
|
+
];
|
|
597
|
+
const lower = task.toLowerCase();
|
|
598
|
+
for (const kw of techKeywords) {
|
|
599
|
+
if (lower.includes(kw)) entities.push(kw);
|
|
600
|
+
}
|
|
601
|
+
// Also extract capitalized proper nouns (file names, project names)
|
|
602
|
+
const nouns = task.match(/\b[A-Z][a-z]{2,}\b/g);
|
|
603
|
+
if (nouns) entities.push(...nouns.map(n => n.toLowerCase()).slice(0, 3));
|
|
604
|
+
|
|
605
|
+
if (entities.length > 0) {
|
|
606
|
+
const facts = await this.deps.queryKnowledgeFacts(entities.slice(0, 5), 5);
|
|
607
|
+
if (facts && facts.trim().length > 20) {
|
|
608
|
+
knowledgeSection = `\n## Relevant Knowledge\n${facts.trim()}`;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} catch (err) {
|
|
612
|
+
log(TAG, `spawn knowledge enrichment failed: ${String(err)}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Include latest digest for screen/audio context
|
|
617
|
+
const latestDigest = this.deps.feedBuffer.latest()?.text;
|
|
618
|
+
const contextSection = latestDigest
|
|
619
|
+
? `\n## Current User Context\n${latestDigest.slice(0, 500)}`
|
|
620
|
+
: "";
|
|
621
|
+
|
|
586
622
|
return [
|
|
587
623
|
"# Subagent Context",
|
|
588
624
|
"",
|
|
@@ -596,8 +632,9 @@ ${recentLines.join("\n")}`;
|
|
|
596
632
|
"1. Stay focused — do your assigned task, nothing else",
|
|
597
633
|
"2. Your final message will be reported to the requester",
|
|
598
634
|
"3. Be concise but informative",
|
|
599
|
-
"",
|
|
600
|
-
|
|
635
|
+
label ? `\nLabel: ${label}` : "",
|
|
636
|
+
knowledgeSection,
|
|
637
|
+
contextSection,
|
|
601
638
|
].filter(Boolean).join("\n");
|
|
602
639
|
}
|
|
603
640
|
|
|
@@ -633,19 +670,9 @@ ${recentLines.join("\n")}`;
|
|
|
633
670
|
return;
|
|
634
671
|
}
|
|
635
672
|
|
|
636
|
-
const maxWaitMs = 5 * 60 * 1000; // 5 minutes
|
|
637
673
|
const pollIntervalMs = 5000; // 5 seconds
|
|
638
674
|
|
|
639
675
|
const poll = async (): Promise<void> => {
|
|
640
|
-
const elapsed = Date.now() - task.startedAt;
|
|
641
|
-
if (elapsed > maxWaitMs) {
|
|
642
|
-
log(TAG, `spawn-task timeout: taskId=${taskId}`);
|
|
643
|
-
this.broadcastTaskEvent(taskId, "timeout", task.label, task.startedAt);
|
|
644
|
-
this.pendingSpawnTasks.delete(taskId);
|
|
645
|
-
savePendingTasks(this.pendingSpawnTasks);
|
|
646
|
-
this.finishPoll();
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
676
|
|
|
650
677
|
if (!this.wsClient.isConnected) {
|
|
651
678
|
// Retry later
|