@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.2
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/dist/heart/core.js +32 -2
- package/dist/heart/daemon/daemon-cli.js +68 -19
- package/dist/heart/daemon/daemon.js +3 -0
- package/dist/heart/daemon/hatch-flow.js +2 -1
- package/dist/heart/daemon/log-tailer.js +146 -0
- package/dist/heart/daemon/os-cron.js +260 -0
- package/dist/heart/daemon/process-manager.js +18 -1
- package/dist/heart/daemon/task-scheduler.js +4 -1
- package/dist/heart/identity.js +14 -3
- package/dist/mind/associative-recall.js +23 -2
- package/dist/mind/context.js +85 -1
- package/dist/mind/memory.js +62 -0
- package/dist/mind/pending.js +93 -0
- package/dist/mind/prompt-refresh.js +20 -0
- package/dist/mind/prompt.js +98 -0
- package/dist/nerves/coverage/file-completeness.js +14 -4
- package/dist/repertoire/tools-base.js +92 -0
- package/dist/senses/cli.js +89 -8
- package/dist/senses/inner-dialog.js +15 -0
- package/dist/senses/session-lock.js +119 -0
- package/dist/senses/teams.js +1 -0
- package/package.json +1 -1
- package/subagents/README.md +3 -1
- package/subagents/work-merger.md +33 -2
|
@@ -103,6 +103,7 @@ class TaskDrivenScheduler {
|
|
|
103
103
|
readFileSync;
|
|
104
104
|
writeFileSync;
|
|
105
105
|
readdirSync;
|
|
106
|
+
osCronManager;
|
|
106
107
|
jobs = new Map();
|
|
107
108
|
taskPathByKey = new Map();
|
|
108
109
|
constructor(options) {
|
|
@@ -113,12 +114,13 @@ class TaskDrivenScheduler {
|
|
|
113
114
|
this.readFileSync = options.readFileSync ?? fs.readFileSync;
|
|
114
115
|
this.writeFileSync = options.writeFileSync ?? fs.writeFileSync;
|
|
115
116
|
this.readdirSync = options.readdirSync ?? fs.readdirSync;
|
|
117
|
+
this.osCronManager = options.osCronManager;
|
|
116
118
|
}
|
|
117
119
|
start() {
|
|
118
120
|
void this.reconcile();
|
|
119
121
|
}
|
|
120
122
|
stop() {
|
|
121
|
-
|
|
123
|
+
this.osCronManager?.removeAll();
|
|
122
124
|
}
|
|
123
125
|
listJobs() {
|
|
124
126
|
return [...this.jobs.values()]
|
|
@@ -197,6 +199,7 @@ class TaskDrivenScheduler {
|
|
|
197
199
|
message: "reconciled task-driven schedule jobs",
|
|
198
200
|
meta: { jobCount: this.jobs.size, agents: this.agents.length },
|
|
199
201
|
});
|
|
202
|
+
this.osCronManager?.sync([...this.jobs.values()]);
|
|
200
203
|
}
|
|
201
204
|
async recordTaskRun(agent, taskId) {
|
|
202
205
|
const key = `${agent}:${taskId}`;
|
package/dist/heart/identity.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.getAgentBundlesRoot = getAgentBundlesRoot;
|
|
|
41
41
|
exports.getAgentRoot = getAgentRoot;
|
|
42
42
|
exports.getAgentSecretsPath = getAgentSecretsPath;
|
|
43
43
|
exports.loadAgentConfig = loadAgentConfig;
|
|
44
|
+
exports.setAgentName = setAgentName;
|
|
44
45
|
exports.resetIdentity = resetIdentity;
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
46
47
|
const os = __importStar(require("os"));
|
|
@@ -100,11 +101,11 @@ function getAgentName() {
|
|
|
100
101
|
}
|
|
101
102
|
/**
|
|
102
103
|
* Resolve repo root from __dirname.
|
|
103
|
-
* In dev (tsx): __dirname is `<repo>/src`, so repo root is
|
|
104
|
-
* In compiled (node dist/): __dirname is `<repo>/dist`, so repo root is
|
|
104
|
+
* In dev (tsx): __dirname is `<repo>/src/heart`, so repo root is two levels up.
|
|
105
|
+
* In compiled (node dist/): __dirname is `<repo>/dist/heart`, so repo root is two levels up.
|
|
105
106
|
*/
|
|
106
107
|
function getRepoRoot() {
|
|
107
|
-
return path.resolve(__dirname, "
|
|
108
|
+
return path.resolve(__dirname, "../..");
|
|
108
109
|
}
|
|
109
110
|
/**
|
|
110
111
|
* Returns the shared bundle root directory: `~/AgentBundles/`
|
|
@@ -250,6 +251,7 @@ function loadAgentConfig() {
|
|
|
250
251
|
enabled,
|
|
251
252
|
provider: rawProvider,
|
|
252
253
|
context: parsed.context,
|
|
254
|
+
logging: parsed.logging,
|
|
253
255
|
phrases: parsed.phrases,
|
|
254
256
|
};
|
|
255
257
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -260,6 +262,15 @@ function loadAgentConfig() {
|
|
|
260
262
|
});
|
|
261
263
|
return _cachedAgentConfig;
|
|
262
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Prime the agent name cache explicitly.
|
|
267
|
+
* Used when agent name is known via parameter (e.g., `ouro` CLI routing)
|
|
268
|
+
* rather than `--agent` argv. All downstream calls to `getAgentName()`
|
|
269
|
+
* will return this value until `resetIdentity()` is called.
|
|
270
|
+
*/
|
|
271
|
+
function setAgentName(name) {
|
|
272
|
+
_cachedAgentName = name;
|
|
273
|
+
}
|
|
263
274
|
/**
|
|
264
275
|
* Clear all cached identity state.
|
|
265
276
|
* Used in tests and when switching agent context.
|
|
@@ -144,8 +144,29 @@ async function injectAssociativeRecall(messages, options) {
|
|
|
144
144
|
const facts = readFacts(memoryRoot);
|
|
145
145
|
if (facts.length === 0)
|
|
146
146
|
return;
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
let recalled;
|
|
148
|
+
try {
|
|
149
|
+
const provider = options?.provider ?? createDefaultProvider();
|
|
150
|
+
recalled = await recallFactsForQuery(query, facts, provider, options);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Embeddings unavailable — fall back to substring matching
|
|
154
|
+
const lowerQuery = query.toLowerCase();
|
|
155
|
+
const topK = options?.topK ?? DEFAULT_TOP_K;
|
|
156
|
+
recalled = facts
|
|
157
|
+
.filter((fact) => fact.text.toLowerCase().includes(lowerQuery))
|
|
158
|
+
.slice(0, topK)
|
|
159
|
+
.map((fact) => ({ ...fact, score: 1 }));
|
|
160
|
+
if (recalled.length > 0) {
|
|
161
|
+
(0, runtime_1.emitNervesEvent)({
|
|
162
|
+
level: "warn",
|
|
163
|
+
component: "mind",
|
|
164
|
+
event: "mind.associative_recall_fallback",
|
|
165
|
+
message: "embeddings unavailable, used substring fallback",
|
|
166
|
+
meta: { matchCount: recalled.length },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
149
170
|
if (recalled.length === 0)
|
|
150
171
|
return;
|
|
151
172
|
const recallSection = recalled
|
package/dist/mind/context.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.trimMessages = trimMessages;
|
|
37
|
+
exports.validateSessionMessages = validateSessionMessages;
|
|
38
|
+
exports.repairSessionMessages = repairSessionMessages;
|
|
37
39
|
exports.saveSession = saveSession;
|
|
38
40
|
exports.loadSession = loadSession;
|
|
39
41
|
exports.postTurn = postTurn;
|
|
@@ -166,7 +168,77 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
|
166
168
|
});
|
|
167
169
|
return trimmed;
|
|
168
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Checks session invariant: after system messages, sequence must be
|
|
173
|
+
* user → assistant (with optional tool calls/results) → user → assistant...
|
|
174
|
+
* Never assistant → assistant without a user in between.
|
|
175
|
+
*/
|
|
176
|
+
function validateSessionMessages(messages) {
|
|
177
|
+
const violations = [];
|
|
178
|
+
let prevNonToolRole = null;
|
|
179
|
+
let prevAssistantHadToolCalls = false;
|
|
180
|
+
let sawToolResultSincePrevAssistant = false;
|
|
181
|
+
for (let i = 0; i < messages.length; i++) {
|
|
182
|
+
const msg = messages[i];
|
|
183
|
+
if (msg.role === "system")
|
|
184
|
+
continue;
|
|
185
|
+
if (msg.role === "tool") {
|
|
186
|
+
sawToolResultSincePrevAssistant = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (msg.role === "assistant" && prevNonToolRole === "assistant") {
|
|
190
|
+
// assistant → tool(s) → assistant is valid (tool call flow)
|
|
191
|
+
if (!(prevAssistantHadToolCalls && sawToolResultSincePrevAssistant)) {
|
|
192
|
+
violations.push(`back-to-back assistant at index ${i}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
prevAssistantHadToolCalls = msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
|
|
196
|
+
sawToolResultSincePrevAssistant = false;
|
|
197
|
+
prevNonToolRole = msg.role;
|
|
198
|
+
}
|
|
199
|
+
return violations;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Repairs session invariant violations by merging consecutive assistant messages.
|
|
203
|
+
*/
|
|
204
|
+
function repairSessionMessages(messages) {
|
|
205
|
+
const violations = validateSessionMessages(messages);
|
|
206
|
+
if (violations.length === 0)
|
|
207
|
+
return messages;
|
|
208
|
+
const result = [];
|
|
209
|
+
for (const msg of messages) {
|
|
210
|
+
if (msg.role === "assistant" && result.length > 0) {
|
|
211
|
+
const prev = result[result.length - 1];
|
|
212
|
+
if (prev.role === "assistant" && !("tool_calls" in prev)) {
|
|
213
|
+
const prevContent = typeof prev.content === "string" ? prev.content : "";
|
|
214
|
+
const curContent = typeof msg.content === "string" ? msg.content : "";
|
|
215
|
+
prev.content = `${prevContent}\n\n${curContent}`;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
result.push(msg);
|
|
220
|
+
}
|
|
221
|
+
(0, runtime_1.emitNervesEvent)({
|
|
222
|
+
level: "warn",
|
|
223
|
+
event: "mind.session_invariant_repair",
|
|
224
|
+
component: "mind",
|
|
225
|
+
message: "repaired session invariant violations",
|
|
226
|
+
meta: { violations },
|
|
227
|
+
});
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
169
230
|
function saveSession(filePath, messages, lastUsage) {
|
|
231
|
+
const violations = validateSessionMessages(messages);
|
|
232
|
+
if (violations.length > 0) {
|
|
233
|
+
(0, runtime_1.emitNervesEvent)({
|
|
234
|
+
level: "warn",
|
|
235
|
+
event: "mind.session_invariant_violation",
|
|
236
|
+
component: "mind",
|
|
237
|
+
message: "session invariant violated on save",
|
|
238
|
+
meta: { path: filePath, violations },
|
|
239
|
+
});
|
|
240
|
+
messages = repairSessionMessages(messages);
|
|
241
|
+
}
|
|
170
242
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
171
243
|
const envelope = { version: 1, messages };
|
|
172
244
|
if (lastUsage)
|
|
@@ -179,7 +251,19 @@ function loadSession(filePath) {
|
|
|
179
251
|
const data = JSON.parse(raw);
|
|
180
252
|
if (data.version !== 1)
|
|
181
253
|
return null;
|
|
182
|
-
|
|
254
|
+
let messages = data.messages;
|
|
255
|
+
const violations = validateSessionMessages(messages);
|
|
256
|
+
if (violations.length > 0) {
|
|
257
|
+
(0, runtime_1.emitNervesEvent)({
|
|
258
|
+
level: "warn",
|
|
259
|
+
event: "mind.session_invariant_violation",
|
|
260
|
+
component: "mind",
|
|
261
|
+
message: "session invariant violated on load",
|
|
262
|
+
meta: { path: filePath, violations },
|
|
263
|
+
});
|
|
264
|
+
messages = repairSessionMessages(messages);
|
|
265
|
+
}
|
|
266
|
+
return { messages, lastUsage: data.lastUsage };
|
|
183
267
|
}
|
|
184
268
|
catch {
|
|
185
269
|
return null;
|
package/dist/mind/memory.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.ensureMemoryStorePaths = ensureMemoryStorePaths;
|
|
|
38
38
|
exports.appendFactsWithDedup = appendFactsWithDedup;
|
|
39
39
|
exports.readMemoryFacts = readMemoryFacts;
|
|
40
40
|
exports.saveMemoryFact = saveMemoryFact;
|
|
41
|
+
exports.backfillEmbeddings = backfillEmbeddings;
|
|
41
42
|
exports.searchMemoryFacts = searchMemoryFacts;
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
43
44
|
const path = __importStar(require("path"));
|
|
@@ -265,6 +266,67 @@ async function saveMemoryFact(options) {
|
|
|
265
266
|
};
|
|
266
267
|
return appendFactsWithDedup(stores, [fact]);
|
|
267
268
|
}
|
|
269
|
+
async function backfillEmbeddings(options) {
|
|
270
|
+
const memoryRoot = options?.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
|
|
271
|
+
const factsPath = path.join(memoryRoot, "facts.jsonl");
|
|
272
|
+
if (!fs.existsSync(factsPath))
|
|
273
|
+
return { total: 0, backfilled: 0, failed: 0 };
|
|
274
|
+
const facts = readExistingFacts(factsPath);
|
|
275
|
+
const needsEmbedding = facts.filter((f) => !Array.isArray(f.embedding) || f.embedding.length === 0);
|
|
276
|
+
if (needsEmbedding.length === 0)
|
|
277
|
+
return { total: facts.length, backfilled: 0, failed: 0 };
|
|
278
|
+
const provider = options?.embeddingProvider ?? createDefaultEmbeddingProvider();
|
|
279
|
+
if (!provider) {
|
|
280
|
+
(0, runtime_1.emitNervesEvent)({
|
|
281
|
+
level: "warn",
|
|
282
|
+
component: "mind",
|
|
283
|
+
event: "mind.memory_backfill_skipped",
|
|
284
|
+
message: "embedding provider unavailable for backfill",
|
|
285
|
+
meta: { needsEmbedding: needsEmbedding.length },
|
|
286
|
+
});
|
|
287
|
+
return { total: facts.length, backfilled: 0, failed: needsEmbedding.length };
|
|
288
|
+
}
|
|
289
|
+
const batchSize = options?.batchSize ?? 50;
|
|
290
|
+
let backfilled = 0;
|
|
291
|
+
let failed = 0;
|
|
292
|
+
for (let i = 0; i < needsEmbedding.length; i += batchSize) {
|
|
293
|
+
const batch = needsEmbedding.slice(i, i + batchSize);
|
|
294
|
+
try {
|
|
295
|
+
const vectors = await provider.embed(batch.map((f) => f.text));
|
|
296
|
+
for (let j = 0; j < batch.length; j++) {
|
|
297
|
+
batch[j].embedding = vectors[j] ?? [];
|
|
298
|
+
if (batch[j].embedding.length > 0)
|
|
299
|
+
backfilled++;
|
|
300
|
+
else
|
|
301
|
+
failed++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
failed += batch.length;
|
|
306
|
+
(0, runtime_1.emitNervesEvent)({
|
|
307
|
+
level: "warn",
|
|
308
|
+
component: "mind",
|
|
309
|
+
event: "mind.memory_backfill_batch_error",
|
|
310
|
+
message: "embedding backfill batch failed",
|
|
311
|
+
meta: {
|
|
312
|
+
batchStart: i,
|
|
313
|
+
batchSize: batch.length,
|
|
314
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Rewrite facts file with updated embeddings
|
|
320
|
+
const lines = facts.map((f) => JSON.stringify(f)).join("\n") + "\n";
|
|
321
|
+
fs.writeFileSync(factsPath, lines, "utf8");
|
|
322
|
+
(0, runtime_1.emitNervesEvent)({
|
|
323
|
+
component: "mind",
|
|
324
|
+
event: "mind.memory_backfill_complete",
|
|
325
|
+
message: "embedding backfill completed",
|
|
326
|
+
meta: { total: facts.length, backfilled, failed },
|
|
327
|
+
});
|
|
328
|
+
return { total: facts.length, backfilled, failed };
|
|
329
|
+
}
|
|
268
330
|
function substringMatches(queryLower, facts) {
|
|
269
331
|
return facts.filter((fact) => fact.text.toLowerCase().includes(queryLower));
|
|
270
332
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getPendingDir = getPendingDir;
|
|
37
|
+
exports.drainPending = drainPending;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
function getPendingDir(agentName, friendId, channel, key) {
|
|
43
|
+
return path.join(os.homedir(), ".agentstate", agentName, "pending", friendId, channel, key);
|
|
44
|
+
}
|
|
45
|
+
function drainPending(pendingDir) {
|
|
46
|
+
if (!fs.existsSync(pendingDir))
|
|
47
|
+
return [];
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(pendingDir);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
// Collect both .json (new) and .processing (crash recovery)
|
|
56
|
+
const jsonFiles = entries.filter(f => f.endsWith(".json") && !f.endsWith(".processing"));
|
|
57
|
+
const processingFiles = entries.filter(f => f.endsWith(".json.processing"));
|
|
58
|
+
// Sort by filename (timestamp prefix gives chronological order)
|
|
59
|
+
const allFiles = [
|
|
60
|
+
...processingFiles.map(f => ({ file: f, needsRename: false })),
|
|
61
|
+
...jsonFiles.map(f => ({ file: f, needsRename: true })),
|
|
62
|
+
].sort((a, b) => a.file.localeCompare(b.file));
|
|
63
|
+
const messages = [];
|
|
64
|
+
for (const { file, needsRename } of allFiles) {
|
|
65
|
+
const srcPath = path.join(pendingDir, file);
|
|
66
|
+
const processingPath = needsRename
|
|
67
|
+
? path.join(pendingDir, file + ".processing")
|
|
68
|
+
: srcPath;
|
|
69
|
+
try {
|
|
70
|
+
if (needsRename) {
|
|
71
|
+
fs.renameSync(srcPath, processingPath);
|
|
72
|
+
}
|
|
73
|
+
const raw = fs.readFileSync(processingPath, "utf-8");
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
messages.push(parsed);
|
|
76
|
+
fs.unlinkSync(processingPath);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Skip unparseable files — still try to clean up
|
|
80
|
+
try {
|
|
81
|
+
fs.unlinkSync(processingPath);
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
(0, runtime_1.emitNervesEvent)({
|
|
87
|
+
event: "mind.pending_drained",
|
|
88
|
+
component: "mind",
|
|
89
|
+
message: "pending queue drained",
|
|
90
|
+
meta: { pendingDir, count: messages.length, recovered: processingFiles.length },
|
|
91
|
+
});
|
|
92
|
+
return messages;
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.refreshSystemPrompt = refreshSystemPrompt;
|
|
4
|
+
const prompt_1 = require("./prompt");
|
|
5
|
+
const runtime_1 = require("../nerves/runtime");
|
|
6
|
+
async function refreshSystemPrompt(messages, channel, options, context) {
|
|
7
|
+
const newSystem = await (0, prompt_1.buildSystem)(channel, options, context);
|
|
8
|
+
if (messages.length > 0 && messages[0].role === "system") {
|
|
9
|
+
messages[0] = { role: "system", content: newSystem };
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
messages.unshift({ role: "system", content: newSystem });
|
|
13
|
+
}
|
|
14
|
+
(0, runtime_1.emitNervesEvent)({
|
|
15
|
+
event: "mind.system_prompt_refreshed",
|
|
16
|
+
component: "mind",
|
|
17
|
+
message: "system prompt refreshed",
|
|
18
|
+
meta: { channel },
|
|
19
|
+
});
|
|
20
|
+
}
|
package/dist/mind/prompt.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.resetPsycheCache = resetPsycheCache;
|
|
37
|
+
exports.buildSessionSummary = buildSessionSummary;
|
|
37
38
|
exports.runtimeInfoSection = runtimeInfoSection;
|
|
38
39
|
exports.contextSection = contextSection;
|
|
39
40
|
exports.buildSystem = buildSystem;
|
|
@@ -43,6 +44,7 @@ const core_1 = require("../heart/core");
|
|
|
43
44
|
const tools_1 = require("../repertoire/tools");
|
|
44
45
|
const skills_1 = require("../repertoire/skills");
|
|
45
46
|
const identity_1 = require("../heart/identity");
|
|
47
|
+
const os = __importStar(require("os"));
|
|
46
48
|
const channel_1 = require("./friends/channel");
|
|
47
49
|
const runtime_1 = require("../nerves/runtime");
|
|
48
50
|
const first_impressions_1 = require("./first-impressions");
|
|
@@ -73,6 +75,94 @@ function loadPsyche() {
|
|
|
73
75
|
function resetPsycheCache() {
|
|
74
76
|
_psycheCache = null;
|
|
75
77
|
}
|
|
78
|
+
const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
79
|
+
function resolveFriendName(friendId, friendsDir, agentName) {
|
|
80
|
+
if (friendId === "self")
|
|
81
|
+
return agentName;
|
|
82
|
+
try {
|
|
83
|
+
const raw = fs.readFileSync(path.join(friendsDir, `${friendId}.json`), "utf-8");
|
|
84
|
+
const record = JSON.parse(raw);
|
|
85
|
+
return record.name ?? friendId;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return friendId;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function buildSessionSummary(options) {
|
|
92
|
+
const { sessionsDir, friendsDir, agentName, currentFriendId, currentChannel, currentKey, activeThresholdMs = DEFAULT_ACTIVE_THRESHOLD_MS, } = options;
|
|
93
|
+
if (!fs.existsSync(sessionsDir))
|
|
94
|
+
return "";
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const entries = [];
|
|
97
|
+
let friendDirs;
|
|
98
|
+
try {
|
|
99
|
+
friendDirs = fs.readdirSync(sessionsDir);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
for (const friendId of friendDirs) {
|
|
105
|
+
const friendPath = path.join(sessionsDir, friendId);
|
|
106
|
+
let channels;
|
|
107
|
+
try {
|
|
108
|
+
channels = fs.readdirSync(friendPath);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const channel of channels) {
|
|
114
|
+
const channelPath = path.join(friendPath, channel);
|
|
115
|
+
let keys;
|
|
116
|
+
try {
|
|
117
|
+
keys = fs.readdirSync(channelPath);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const keyFile of keys) {
|
|
123
|
+
if (!keyFile.endsWith(".json"))
|
|
124
|
+
continue;
|
|
125
|
+
const key = keyFile.replace(/\.json$/, "");
|
|
126
|
+
// Exclude current session
|
|
127
|
+
if (friendId === currentFriendId && channel === currentChannel && key === currentKey) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const filePath = path.join(channelPath, keyFile);
|
|
131
|
+
let mtimeMs;
|
|
132
|
+
try {
|
|
133
|
+
mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (now - mtimeMs > activeThresholdMs)
|
|
139
|
+
continue;
|
|
140
|
+
const displayName = resolveFriendName(friendId, friendsDir, agentName);
|
|
141
|
+
entries.push({ friendId, displayName, channel, key, lastActivityMs: mtimeMs });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (entries.length === 0)
|
|
146
|
+
return "";
|
|
147
|
+
// Sort by most recent first
|
|
148
|
+
entries.sort((a, b) => b.lastActivityMs - a.lastActivityMs);
|
|
149
|
+
const lines = ["## active sessions"];
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const ago = formatTimeAgo(now - entry.lastActivityMs);
|
|
152
|
+
lines.push(`- ${entry.displayName}/${entry.channel}/${entry.key} (last: ${ago})`);
|
|
153
|
+
}
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
function formatTimeAgo(ms) {
|
|
157
|
+
const minutes = Math.floor(ms / 60000);
|
|
158
|
+
if (minutes < 60)
|
|
159
|
+
return `${minutes}m ago`;
|
|
160
|
+
const hours = Math.floor(minutes / 60);
|
|
161
|
+
if (hours < 24)
|
|
162
|
+
return `${hours}h ago`;
|
|
163
|
+
const days = Math.floor(hours / 24);
|
|
164
|
+
return `${days}d ago`;
|
|
165
|
+
}
|
|
76
166
|
function soulSection() {
|
|
77
167
|
return loadPsyche().soul;
|
|
78
168
|
}
|
|
@@ -238,6 +328,14 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
238
328
|
toolsSection(channel, options),
|
|
239
329
|
skillsSection(),
|
|
240
330
|
taskBoardSection(),
|
|
331
|
+
buildSessionSummary({
|
|
332
|
+
sessionsDir: path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions"),
|
|
333
|
+
friendsDir: path.join((0, identity_1.getAgentRoot)(), "friends"),
|
|
334
|
+
agentName: (0, identity_1.getAgentName)(),
|
|
335
|
+
currentFriendId: context?.friend?.id,
|
|
336
|
+
currentChannel: channel,
|
|
337
|
+
currentKey: "session",
|
|
338
|
+
}),
|
|
241
339
|
memoryFriendToolContractSection(),
|
|
242
340
|
toolBehaviorSection(options),
|
|
243
341
|
contextSection(context),
|
|
@@ -11,12 +11,22 @@ exports.isTypeOnlyFile = isTypeOnlyFile;
|
|
|
11
11
|
exports.checkFileCompleteness = checkFileCompleteness;
|
|
12
12
|
/**
|
|
13
13
|
* Determines if a source file is type-only (no executable code).
|
|
14
|
-
* A file is type-only if it contains no function, class, or
|
|
14
|
+
* A file is type-only if it contains no function, class, or mutable declarations.
|
|
15
|
+
* `const ... as const` declarations are treated as type-equivalent (frozen
|
|
16
|
+
* compile-time values with no side effects).
|
|
15
17
|
*/
|
|
16
18
|
function isTypeOnlyFile(source) {
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
const lines = source.split("\n");
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
// Skip lines that are const+as-const (type-equivalent frozen values)
|
|
23
|
+
if (/\bconst\s/.test(trimmed) && /\bas\s+const\b/.test(trimmed))
|
|
24
|
+
continue;
|
|
25
|
+
// Check for executable code markers
|
|
26
|
+
if (/\b(function|class|const|let|var)\s/.test(trimmed))
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
20
30
|
}
|
|
21
31
|
/**
|
|
22
32
|
* Check that all production files have at least one emitNervesEvent call.
|
|
@@ -41,6 +41,7 @@ const skills_1 = require("./skills");
|
|
|
41
41
|
const config_1 = require("../heart/config");
|
|
42
42
|
const runtime_1 = require("../nerves/runtime");
|
|
43
43
|
const identity_1 = require("../heart/identity");
|
|
44
|
+
const os = __importStar(require("os"));
|
|
44
45
|
const tasks_1 = require("./tasks");
|
|
45
46
|
const tools_1 = require("./coding/tools");
|
|
46
47
|
const memory_1 = require("../mind/memory");
|
|
@@ -602,6 +603,97 @@ exports.baseToolDefinitions = [
|
|
|
602
603
|
}
|
|
603
604
|
},
|
|
604
605
|
},
|
|
606
|
+
// -- cross-session awareness --
|
|
607
|
+
{
|
|
608
|
+
tool: {
|
|
609
|
+
type: "function",
|
|
610
|
+
function: {
|
|
611
|
+
name: "query_session",
|
|
612
|
+
description: "read the last messages from another session. use this to check on a conversation with a friend or review your own inner dialog.",
|
|
613
|
+
parameters: {
|
|
614
|
+
type: "object",
|
|
615
|
+
properties: {
|
|
616
|
+
friendId: { type: "string", description: "the friend UUID (or 'self')" },
|
|
617
|
+
channel: { type: "string", description: "the channel: cli, teams, or inner" },
|
|
618
|
+
key: { type: "string", description: "session key (defaults to 'session')" },
|
|
619
|
+
messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
|
|
620
|
+
},
|
|
621
|
+
required: ["friendId", "channel"],
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
handler: async (args, ctx) => {
|
|
626
|
+
try {
|
|
627
|
+
const friendId = args.friendId;
|
|
628
|
+
const channel = args.channel;
|
|
629
|
+
const key = args.key || "session";
|
|
630
|
+
const count = parseInt(args.messageCount || "20", 10);
|
|
631
|
+
const sessFile = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel, `${key}.json`);
|
|
632
|
+
const raw = fs.readFileSync(sessFile, "utf-8");
|
|
633
|
+
const data = JSON.parse(raw);
|
|
634
|
+
const messages = (data.messages || [])
|
|
635
|
+
.filter((m) => m.role !== "system");
|
|
636
|
+
const tail = messages.slice(-count);
|
|
637
|
+
if (tail.length === 0)
|
|
638
|
+
return "session exists but has no non-system messages.";
|
|
639
|
+
const transcript = tail.map((m) => `[${m.role}] ${m.content}`).join("\n");
|
|
640
|
+
// LLM summarization when summarize function is available
|
|
641
|
+
if (ctx?.summarize) {
|
|
642
|
+
const trustLevel = ctx.context?.friend?.trustLevel ?? "family";
|
|
643
|
+
const isSelfQuery = friendId === "self";
|
|
644
|
+
const instruction = isSelfQuery
|
|
645
|
+
? "summarize this session transcript fully and transparently. this is my own inner dialog — include all details, decisions, and reasoning."
|
|
646
|
+
: `summarize this session transcript. the person asking has trust level: ${trustLevel}. family=full transparency, friend=share work and general topics but protect other people's identities, acquaintance=very guarded minimal disclosure.`;
|
|
647
|
+
return await ctx.summarize(transcript, instruction);
|
|
648
|
+
}
|
|
649
|
+
return transcript;
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return "no session found for that friend/channel/key combination.";
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
tool: {
|
|
658
|
+
type: "function",
|
|
659
|
+
function: {
|
|
660
|
+
name: "send_message",
|
|
661
|
+
description: "send a message to a friend's session. the message is queued as a pending file and delivered when the target session drains its queue.",
|
|
662
|
+
parameters: {
|
|
663
|
+
type: "object",
|
|
664
|
+
properties: {
|
|
665
|
+
friendId: { type: "string", description: "the friend UUID (or 'self')" },
|
|
666
|
+
channel: { type: "string", description: "the channel: cli, teams, or inner" },
|
|
667
|
+
key: { type: "string", description: "session key (defaults to 'session')" },
|
|
668
|
+
content: { type: "string", description: "the message content to send" },
|
|
669
|
+
},
|
|
670
|
+
required: ["friendId", "channel", "content"],
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
handler: async (args) => {
|
|
675
|
+
const friendId = args.friendId;
|
|
676
|
+
const channel = args.channel;
|
|
677
|
+
const key = args.key || "session";
|
|
678
|
+
const content = args.content;
|
|
679
|
+
const now = Date.now();
|
|
680
|
+
const pendingDir = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "pending", friendId, channel, key);
|
|
681
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
682
|
+
const fileName = `${now}-${Math.random().toString(36).slice(2, 10)}.json`;
|
|
683
|
+
const filePath = path.join(pendingDir, fileName);
|
|
684
|
+
const envelope = {
|
|
685
|
+
from: (0, identity_1.getAgentName)(),
|
|
686
|
+
friendId,
|
|
687
|
+
channel,
|
|
688
|
+
key,
|
|
689
|
+
content,
|
|
690
|
+
timestamp: now,
|
|
691
|
+
};
|
|
692
|
+
fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
|
|
693
|
+
const preview = content.length > 80 ? content.slice(0, 80) + "…" : content;
|
|
694
|
+
return `message queued for delivery to ${friendId} on ${channel}/${key}. preview: "${preview}". it will be delivered when their session is next active.`;
|
|
695
|
+
},
|
|
696
|
+
},
|
|
605
697
|
...tools_1.codingToolDefinitions,
|
|
606
698
|
];
|
|
607
699
|
// Backward-compat: extract just the OpenAI tool schemas
|