@geravant/sinain 1.10.1 → 1.12.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 +1 -1
- package/sinain-agent/run.sh +66 -7
- 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 +44 -16
- package/sinain-core/src/escalation/message-builder.ts +45 -118
- package/sinain-core/src/index.ts +20 -36
- package/sinain-core/src/learning/local-curation.ts +4 -4
- package/sinain-core/src/overlay/commands.ts +46 -13
- package/sinain-core/src/overlay/ws-handler.ts +13 -1
- package/sinain-core/src/server.ts +121 -0
- package/sinain-core/src/types.ts +25 -28
- package/sinain-mcp-server/index.ts +28 -0
- package/sinain-memory/__pycache__/graph_query.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
- package/sinain-memory/eval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/assertions.py +0 -21
- package/sinain-memory/eval/benchmarks/__init__.py +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/base_adapter.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/config.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/evaluate.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/ingest.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/longmemeval_adapter.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/query.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/report.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/runner.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/base_adapter.py +43 -0
- package/sinain-memory/eval/benchmarks/config.py +23 -0
- package/sinain-memory/eval/benchmarks/evaluate.py +146 -0
- package/sinain-memory/eval/benchmarks/ingest.py +152 -0
- package/sinain-memory/eval/benchmarks/judges/__init__.py +0 -0
- package/sinain-memory/eval/benchmarks/judges/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/judges/__pycache__/qa_judge.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/judges/qa_judge.py +81 -0
- package/sinain-memory/eval/benchmarks/longmemeval_adapter.py +177 -0
- package/sinain-memory/eval/benchmarks/query.py +172 -0
- package/sinain-memory/eval/benchmarks/report.py +87 -0
- package/sinain-memory/eval/benchmarks/runner.py +276 -0
- package/sinain-memory/koog-config.json +11 -0
- package/sinain-core/src/agent/traits.ts +0 -520
package/package.json
CHANGED
package/sinain-agent/CLAUDE.md
CHANGED
|
@@ -103,7 +103,7 @@ Knowledge API (localhost:9500) ← merges both DBs ← queries
|
|
|
103
103
|
|
|
104
104
|
### Using Knowledge in Escalation Responses
|
|
105
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(
|
|
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
107
|
|
|
108
108
|
## Spawning Background Tasks
|
|
109
109
|
|
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"
|
|
@@ -234,6 +275,11 @@ Knowledge context: sinain-core maintains two knowledge databases — local (sess
|
|
|
234
275
|
while true; do
|
|
235
276
|
# Poll for pending escalation
|
|
236
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
|
|
237
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)
|
|
238
284
|
|
|
239
285
|
if [ -n "$ESC_ID" ]; then
|
|
@@ -274,10 +320,23 @@ while true; do
|
|
|
274
320
|
|
|
275
321
|
if agent_has_mcp; then
|
|
276
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)
|
|
277
331
|
SPAWN_PROMPT="You have a background task to complete. Task: $SPAWN_TASK
|
|
278
|
-
|
|
279
|
-
|
|
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"
|
|
280
338
|
SPAWN_RESULT=$(invoke_agent "$SPAWN_PROMPT" "$SPAWN_MAX_TURNS" || echo "ERROR: agent invocation failed")
|
|
339
|
+
unset SINAIN_SPAWN SINAIN_SPAWN_TASK_ID
|
|
281
340
|
else
|
|
282
341
|
# Pipe path: agent gets task text directly
|
|
283
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
|
}
|
|
@@ -237,6 +237,7 @@ export class Escalator {
|
|
|
237
237
|
escalationReason,
|
|
238
238
|
undefined,
|
|
239
239
|
this.pendingUserCommand ?? undefined,
|
|
240
|
+
this.deps.wsHandler.getState().responseSize ?? "medium",
|
|
240
241
|
);
|
|
241
242
|
|
|
242
243
|
// Clear user command after building the message (consumed once)
|
|
@@ -520,11 +521,11 @@ ${recentLines.join("\n")}`;
|
|
|
520
521
|
message: task,
|
|
521
522
|
sessionKey: childSessionKey,
|
|
522
523
|
lane: "subagent",
|
|
523
|
-
extraSystemPrompt: this.buildChildSystemPrompt(task, label),
|
|
524
|
+
extraSystemPrompt: await this.buildChildSystemPrompt(task, label),
|
|
524
525
|
deliver: false,
|
|
525
526
|
idempotencyKey: idemKey,
|
|
526
527
|
label: label || undefined,
|
|
527
|
-
},
|
|
528
|
+
}, 10 * 60_000, { expectFinal: true });
|
|
528
529
|
|
|
529
530
|
log(TAG, `spawn-task RPC response: ${JSON.stringify(result).slice(0, 500)}`);
|
|
530
531
|
this.stats.totalSpawnResponses++;
|
|
@@ -581,8 +582,44 @@ ${recentLines.join("\n")}`;
|
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
584
|
|
|
584
|
-
/** Build a focused system prompt for the child subagent. */
|
|
585
|
-
private buildChildSystemPrompt(task: string, label?: string): string {
|
|
585
|
+
/** Build a focused system prompt for the child subagent, enriched with knowledge. */
|
|
586
|
+
private async buildChildSystemPrompt(task: string, label?: string): Promise<string> {
|
|
587
|
+
// Fetch relevant knowledge facts (same enrichment as escalations)
|
|
588
|
+
let knowledgeSection = "";
|
|
589
|
+
if (this.deps.queryKnowledgeFacts) {
|
|
590
|
+
try {
|
|
591
|
+
const entities: string[] = [];
|
|
592
|
+
// Extract keywords from task text for entity matching
|
|
593
|
+
const techKeywords = [
|
|
594
|
+
"react-native", "react", "flutter", "swift", "kotlin", "python",
|
|
595
|
+
"typescript", "node", "docker", "sinain", "openclaw", "overlay",
|
|
596
|
+
"sense", "audio", "transcription", "escalation", "knowledge",
|
|
597
|
+
];
|
|
598
|
+
const lower = task.toLowerCase();
|
|
599
|
+
for (const kw of techKeywords) {
|
|
600
|
+
if (lower.includes(kw)) entities.push(kw);
|
|
601
|
+
}
|
|
602
|
+
// Also extract capitalized proper nouns (file names, project names)
|
|
603
|
+
const nouns = task.match(/\b[A-Z][a-z]{2,}\b/g);
|
|
604
|
+
if (nouns) entities.push(...nouns.map(n => n.toLowerCase()).slice(0, 3));
|
|
605
|
+
|
|
606
|
+
if (entities.length > 0) {
|
|
607
|
+
const facts = await this.deps.queryKnowledgeFacts(entities.slice(0, 5), 5);
|
|
608
|
+
if (facts && facts.trim().length > 20) {
|
|
609
|
+
knowledgeSection = `\n## Relevant Knowledge\n${facts.trim()}`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch (err) {
|
|
613
|
+
log(TAG, `spawn knowledge enrichment failed: ${String(err)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Include latest digest for screen/audio context
|
|
618
|
+
const latestDigest = this.deps.feedBuffer.latest()?.text;
|
|
619
|
+
const contextSection = latestDigest
|
|
620
|
+
? `\n## Current User Context\n${latestDigest.slice(0, 500)}`
|
|
621
|
+
: "";
|
|
622
|
+
|
|
586
623
|
return [
|
|
587
624
|
"# Subagent Context",
|
|
588
625
|
"",
|
|
@@ -596,8 +633,9 @@ ${recentLines.join("\n")}`;
|
|
|
596
633
|
"1. Stay focused — do your assigned task, nothing else",
|
|
597
634
|
"2. Your final message will be reported to the requester",
|
|
598
635
|
"3. Be concise but informative",
|
|
599
|
-
"",
|
|
600
|
-
|
|
636
|
+
label ? `\nLabel: ${label}` : "",
|
|
637
|
+
knowledgeSection,
|
|
638
|
+
contextSection,
|
|
601
639
|
].filter(Boolean).join("\n");
|
|
602
640
|
}
|
|
603
641
|
|
|
@@ -633,19 +671,9 @@ ${recentLines.join("\n")}`;
|
|
|
633
671
|
return;
|
|
634
672
|
}
|
|
635
673
|
|
|
636
|
-
const maxWaitMs = 5 * 60 * 1000; // 5 minutes
|
|
637
674
|
const pollIntervalMs = 5000; // 5 seconds
|
|
638
675
|
|
|
639
676
|
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
677
|
|
|
650
678
|
if (!this.wsClient.isConnected) {
|
|
651
679
|
// Retry later
|