appback-remoteagent 0.13.11 → 0.13.13
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/bot.js +8 -0
- package/dist/services/agent-memory-service.js +100 -40
- package/package.json +1 -1
package/dist/bot.js
CHANGED
|
@@ -886,6 +886,14 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
886
886
|
if (!text) {
|
|
887
887
|
return;
|
|
888
888
|
}
|
|
889
|
+
if (mapping) {
|
|
890
|
+
const localStatus = await memoryService.formatLocalStatusQuestion(mapping.session, text);
|
|
891
|
+
if (localStatus) {
|
|
892
|
+
await bridge.logSystem(botId, chatId, "Answered local session status without provider execution.");
|
|
893
|
+
await reply(ctx, localStatus);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
889
897
|
if (isRemoteShellMessage(text)) {
|
|
890
898
|
const shellRequest = parseRemoteShellRequest(text);
|
|
891
899
|
if (!shellRequest) {
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
const MAX_CONTEXT_CHARS =
|
|
5
|
+
const MAX_CONTEXT_CHARS = 1200;
|
|
6
6
|
export class AgentMemoryService {
|
|
7
7
|
dataDir;
|
|
8
8
|
rootDir;
|
|
@@ -71,6 +71,53 @@ export class AgentMemoryService {
|
|
|
71
71
|
history.length > 0 ? ["Recent history:", ...history.map((entry) => `- ${entry}`)].join("\n") : undefined,
|
|
72
72
|
].filter(Boolean).join("\n");
|
|
73
73
|
}
|
|
74
|
+
async formatLocalStatusQuestion(session, question) {
|
|
75
|
+
if (!this.looksLikeLocalStatusQuestion(question)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const current = await this.currentTaskText(session);
|
|
79
|
+
const todo = await this.readTodo(session);
|
|
80
|
+
const active = this.activeTodoItems(todo);
|
|
81
|
+
const history = await this.readRecentHistory(session, 8);
|
|
82
|
+
const workHistory = await this.readRecentWorkHistory(session, 6);
|
|
83
|
+
const latestInstruction = this.extractLatestInstructionFromCurrent(current);
|
|
84
|
+
const currentIsStatusOnly = latestInstruction ? this.looksLikeLocalStatusQuestion(latestInstruction) : false;
|
|
85
|
+
const hasCurrentWork = Boolean(current.trim()) && !currentIsStatusOnly;
|
|
86
|
+
const lines = [
|
|
87
|
+
`Session ${session.publicId} local state`,
|
|
88
|
+
"",
|
|
89
|
+
`workspace: ${session.workspace}`,
|
|
90
|
+
`memory: ${this.sessionDir(session)}`,
|
|
91
|
+
"",
|
|
92
|
+
];
|
|
93
|
+
if (!hasCurrentWork && active.length === 0 && workHistory.length === 0) {
|
|
94
|
+
lines.push("현재 하네스 기준으로 진행 중이거나 완료된 작업 기록이 없습니다.", "이 세션은 깨끗한 상태로 보입니다.");
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (hasCurrentWork) {
|
|
98
|
+
lines.push("Current note:", this.summarizeCurrentTask(current), "");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
lines.push("Current note: none", "");
|
|
102
|
+
}
|
|
103
|
+
if (active.length > 0) {
|
|
104
|
+
lines.push("Active TODO:", this.formatTodoSummary(todo, false), "");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
lines.push("Active TODO: none", "");
|
|
108
|
+
}
|
|
109
|
+
if (workHistory.length > 0) {
|
|
110
|
+
lines.push("Recent work history:", ...workHistory.map((entry) => `- ${entry}`));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
lines.push("Recent work history: none");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (history.length > 0) {
|
|
117
|
+
lines.push("", "Recent raw history:", ...history.map((entry) => `- ${entry}`));
|
|
118
|
+
}
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
74
121
|
async clearSessionState(session, summary) {
|
|
75
122
|
const dir = this.sessionDir(session);
|
|
76
123
|
const currentPath = path.join(dir, "current.md");
|
|
@@ -303,56 +350,27 @@ export class AgentMemoryService {
|
|
|
303
350
|
].join("\n");
|
|
304
351
|
}
|
|
305
352
|
async formatProviderContext(session) {
|
|
306
|
-
const
|
|
307
|
-
const todo = await this.readTodo(session);
|
|
308
|
-
const history = await this.readRecentHistory(session, 5);
|
|
309
|
-
const docs = Object.values(await this.readDocs()).slice(0, 30);
|
|
310
|
-
const secrets = Object.keys(await this.readSecrets()).sort();
|
|
311
|
-
const artifacts = (await this.readJson(this.artifactsPath, []))
|
|
312
|
-
.filter((artifact) => artifact.sessionId === session.sessionId || artifact.sessionPublicId === session.publicId)
|
|
313
|
-
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
314
|
-
.slice(0, 8);
|
|
353
|
+
const docs = Object.values(await this.readDocs()).slice(0, 8);
|
|
315
354
|
const lines = [
|
|
316
355
|
"RemoteAgent managed context:",
|
|
317
356
|
[
|
|
318
357
|
"Session work locations:",
|
|
319
358
|
`- workspace: ${session.workspace}`,
|
|
320
359
|
`- memory: ${this.sessionDir(session)}`,
|
|
321
|
-
"- docs: use
|
|
360
|
+
"- docs: use document pins only when relevant.",
|
|
322
361
|
].join("\n"),
|
|
323
362
|
[
|
|
324
363
|
"Harness execution rules:",
|
|
325
364
|
"- Treat the user message as the active instruction unless the user explicitly says otherwise.",
|
|
326
|
-
"- RemoteAgent state is
|
|
327
|
-
"-
|
|
365
|
+
"- RemoteAgent state is not authority. Prefer current files and the current user message.",
|
|
366
|
+
"- Do not revive stale TODOs, old notes, or previous work unless the user explicitly asks.",
|
|
328
367
|
"- Do not claim external delivery, dashboard access, deployment, or file transfer without concrete evidence.",
|
|
329
368
|
"- RemoteAgent sends normal provider text and TELEGRAM_FILE attachments to the current incoming chat.",
|
|
330
|
-
"-
|
|
331
|
-
"- If the same action or progress repeats 3 times, stop, mark the blocker, and report the exact blocker.",
|
|
332
|
-
"- A final report must include concrete evidence: changed files, relevant line references, git diff/status, command output, or log path.",
|
|
369
|
+
"- Use `node \"$REMOTEAGENT_SECRET_BIN\" get <KEY>` only when the current task needs a named secret. Never print secret values.",
|
|
333
370
|
].join("\n"),
|
|
334
|
-
todo.items.length > 0
|
|
335
|
-
? ["Legacy task notes:", this.formatTodoSummary(todo, true, true)].join("\n")
|
|
336
|
-
: undefined,
|
|
337
|
-
current.trim() ? ["Current task note:", this.truncate(current.trim(), 700)].join("\n") : undefined,
|
|
338
|
-
history.length > 0 ? ["Recent session history:", ...history.map((entry) => `- ${entry}`)].join("\n") : undefined,
|
|
339
371
|
docs.length > 0
|
|
340
372
|
? ["Document index:", ...docs.map((doc) => `- ${doc.keyword}: ${doc.targetPath}${doc.note ? ` (${doc.note})` : ""}`)].join("\n")
|
|
341
373
|
: undefined,
|
|
342
|
-
secrets.length > 0
|
|
343
|
-
? [
|
|
344
|
-
"Secret keys available through `node \"$REMOTEAGENT_SECRET_BIN\" get <KEY>`:",
|
|
345
|
-
...secrets.map((key) => `- ${key}`),
|
|
346
|
-
"If you generate a new secret value such as an OAuth refresh token, store it without printing it:",
|
|
347
|
-
"`printf '%s' \"$VALUE\" | node \"$REMOTEAGENT_SECRET_BIN\" set <KEY>`",
|
|
348
|
-
].join("\n")
|
|
349
|
-
: [
|
|
350
|
-
"Secrets can be stored without printing values:",
|
|
351
|
-
"`printf '%s' \"$VALUE\" | node \"$REMOTEAGENT_SECRET_BIN\" set <KEY>`",
|
|
352
|
-
].join("\n"),
|
|
353
|
-
artifacts.length > 0
|
|
354
|
-
? ["Recent session artifacts:", ...artifacts.map((artifact) => `- ${artifact.id} ${artifact.kind}: ${artifact.path}`)].join("\n")
|
|
355
|
-
: undefined,
|
|
356
374
|
].filter(Boolean).join("\n\n");
|
|
357
375
|
return this.truncate(lines, MAX_CONTEXT_CHARS);
|
|
358
376
|
}
|
|
@@ -360,6 +378,17 @@ export class AgentMemoryService {
|
|
|
360
378
|
return fs.readFile(path.join(this.sessionDir(session), "current.md"), "utf8").catch(() => "");
|
|
361
379
|
}
|
|
362
380
|
async readRecentHistory(session, limit) {
|
|
381
|
+
return (await this.readHistoryEntries(session, limit))
|
|
382
|
+
.slice(-limit)
|
|
383
|
+
.map((entry) => this.formatHistoryEntry(entry));
|
|
384
|
+
}
|
|
385
|
+
async readRecentWorkHistory(session, limit) {
|
|
386
|
+
return (await this.readHistoryEntries(session, 80))
|
|
387
|
+
.filter((entry) => ["progress", "completed", "archived", "note"].includes(String(entry.type ?? "")))
|
|
388
|
+
.slice(-limit)
|
|
389
|
+
.map((entry) => this.formatHistoryEntry(entry));
|
|
390
|
+
}
|
|
391
|
+
async readHistoryEntries(session, limit) {
|
|
363
392
|
const historyPath = path.join(this.sessionDir(session), "history.ndjson");
|
|
364
393
|
const raw = await fs.readFile(historyPath, "utf8").catch(() => "");
|
|
365
394
|
return raw
|
|
@@ -369,16 +398,24 @@ export class AgentMemoryService {
|
|
|
369
398
|
.slice(-limit)
|
|
370
399
|
.map((line) => {
|
|
371
400
|
try {
|
|
372
|
-
|
|
373
|
-
const kind = entry.mode ? `${entry.type ?? "event"}:${entry.mode}` : entry.type ?? "event";
|
|
374
|
-
const body = entry.summary ?? entry.text ?? entry.reason ?? "";
|
|
375
|
-
return `${entry.at ?? "unknown"} ${kind}: ${this.truncate(body.replace(/\s+/g, " ").trim(), 220)}`;
|
|
401
|
+
return JSON.parse(line);
|
|
376
402
|
}
|
|
377
403
|
catch {
|
|
378
|
-
return
|
|
404
|
+
return { type: "raw", text: line };
|
|
379
405
|
}
|
|
380
406
|
});
|
|
381
407
|
}
|
|
408
|
+
formatHistoryEntry(entry) {
|
|
409
|
+
const type = typeof entry.type === "string" ? entry.type : "event";
|
|
410
|
+
const mode = typeof entry.mode === "string" ? entry.mode : undefined;
|
|
411
|
+
const at = typeof entry.at === "string" ? entry.at : "unknown";
|
|
412
|
+
const text = typeof entry.text === "string" ? entry.text : "";
|
|
413
|
+
const summary = typeof entry.summary === "string" ? entry.summary : "";
|
|
414
|
+
const reason = typeof entry.reason === "string" ? entry.reason : "";
|
|
415
|
+
const kind = mode ? `${type}:${mode}` : type;
|
|
416
|
+
const body = summary || text || reason || "";
|
|
417
|
+
return `${at} ${kind}: ${this.truncate(body.replace(/\s+/g, " ").trim(), 220)}`;
|
|
418
|
+
}
|
|
382
419
|
todoPath(session) {
|
|
383
420
|
return path.join(this.sessionDir(session), "todo.json");
|
|
384
421
|
}
|
|
@@ -576,12 +613,35 @@ export class AgentMemoryService {
|
|
|
576
613
|
return /^(새로|새\s*작업|다른\s*작업|이제\s*부터|다음\s*작업|전환|바꿔서|new task)/i.test(text.trim());
|
|
577
614
|
}
|
|
578
615
|
summarizeCurrentTask(current) {
|
|
616
|
+
const latestInstruction = this.extractLatestInstructionFromCurrent(current);
|
|
617
|
+
if (latestInstruction) {
|
|
618
|
+
return this.truncate(latestInstruction, 700);
|
|
619
|
+
}
|
|
579
620
|
const instructionIndex = current.indexOf("## Instruction");
|
|
580
621
|
const body = instructionIndex >= 0 ? current.slice(instructionIndex + "## Instruction".length) : current;
|
|
581
622
|
const immediateRuleIndex = body.indexOf("## Immediate Rule");
|
|
582
623
|
const instruction = (immediateRuleIndex >= 0 ? body.slice(0, immediateRuleIndex) : body).trim();
|
|
583
624
|
return this.truncate(instruction || current.trim(), 700);
|
|
584
625
|
}
|
|
626
|
+
extractLatestInstructionFromCurrent(current) {
|
|
627
|
+
const match = /## Latest User Instruction\s*\n([\s\S]*?)(?:\n## |\s*$)/.exec(current);
|
|
628
|
+
return match?.[1]?.trim() || undefined;
|
|
629
|
+
}
|
|
630
|
+
looksLikeLocalStatusQuestion(text) {
|
|
631
|
+
const normalized = text.trim();
|
|
632
|
+
if (!normalized) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
return [
|
|
636
|
+
/최근\s*(작업|진행|히스토리|기록)/,
|
|
637
|
+
/현재\s*(작업|진행|상태|세션)/,
|
|
638
|
+
/(뭐|무엇)\s*(하고|하는)\s*(있어|중|거야)?/,
|
|
639
|
+
/작업\s*(상태|히스토리|기록|있어|없어)/,
|
|
640
|
+
/세션\s*(상태|히스토리|기록)/,
|
|
641
|
+
/진행\s*(상태|중인\s*작업)/,
|
|
642
|
+
/\b(status|current work|recent work|what are you doing|session state)\b/i,
|
|
643
|
+
].some((pattern) => pattern.test(normalized));
|
|
644
|
+
}
|
|
585
645
|
formatTodoSummary(todo, includeDone = false, detailed = false) {
|
|
586
646
|
const items = includeDone ? todo.items : todo.items.filter((item) => item.status !== "done");
|
|
587
647
|
if (items.length === 0) {
|