Repository navigation
Expand file tree
/
Copy pathconverse.ts
More file actions
153 lines (130 loc) · 5.49 KB
/
Copy pathconverse.ts
File metadata and controls
153 lines (130 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import { chat as askModel, type LlmMessage, type LlmTool } from "./llm.js";
import { attach, history, isReady, recall, remember } from "./memory/index.js";
import type { MemoryScope } from "./memory/types.js";
export const BASE_PROMPT = `You are a friendly assistant.
Keep answers short and plain. Two or three sentences is usually enough.
If you do not know something about the person, say so instead of guessing.`;
/**
* The one tool the model is offered, and only while there is no memory yet.
* It lives here rather than in a caller so every entry point describes it the
* same way to the model.
*/
export const PROVISION_TOOL: LlmTool = {
name: "provision_memory",
description: [
"Create a database so you can remember this person across sessions.",
"",
"Right now you have no memory. Anything said in this conversation is lost",
"when the process stops. Calling this creates a temporary MongoDB cluster,",
"attaches it, and saves everything already said in this session.",
"",
"Call it only when the person explicitly asks you to remember something",
"across sessions, asks whether you will remember them later, or asks you to",
"set up persistent memory. Do not call it just because they share a useful",
"fact, and do not call it for small talk.",
].join("\n"),
parameters: {
type: "object",
properties: {
reason: {
type: "string",
description: "One short sentence on why memory is worth setting up now.",
},
},
required: ["reason"],
},
};
export interface ConverseOptions {
scope: MemoryScope;
message: string;
/**
* Turns said before a database existed. Read as history while there is no
* memory attached, and appended to in place afterwards, so the caller's own
* reference grows too.
*/
sessionBuffer: LlmMessage[];
/**
* What to do if the model asks for a database. Pass this and the model is
* offered provision_memory while no memory is attached; leave it out and the
* model never sees the tool. That is the deterministic-versus-agentic choice.
*/
onProvision?: (reason: string) => Promise<string>;
}
export interface ConverseResult {
reply: string;
recalled: string[];
learned: string[];
}
/**
* Answer one turn: recall what is relevant, ask the model, remember what is
* durable. Both the terminal chat and the web server call this, rather than
* each reimplementing the same turn. That duplication is what once let the web
* version ship without the in-RAM fallback the terminal always had.
*/
export async function converse(options: ConverseOptions): Promise<ConverseResult> {
const { scope, message, sessionBuffer, onProvision } = options;
// No memory yet, and the caller is willing to go get some.
const offerTool = !isReady() && Boolean(onProvision);
const recalled = isReady() ? await recall(scope, message, 5) : [];
const priorTurns: LlmMessage[] = isReady()
? (await history(scope, 10)).map((turn) => ({ role: turn.role, content: turn.content }))
: sessionBuffer.slice(-10);
const reply = await askModel({
system: systemPrompt(recalled.map((memory) => memory.fact), offerTool),
messages: [...priorTurns, { role: "user", content: message }],
...(offerTool
? {
tool: PROVISION_TOOL,
onToolCall: (input) =>
onProvision!(typeof input.reason === "string" ? input.reason : "no reason given"),
}
: {}),
});
let learned: string[] = [];
if (isReady()) {
learned = await remember(scope, "user", message);
await remember(scope, "assistant", reply);
} else {
sessionBuffer.push({ role: "user", content: message }, { role: "assistant", content: reply });
}
return { reply, recalled: recalled.map((memory) => memory.fact), learned };
}
/**
* Attach a cluster mid-conversation and keep what was already said.
*
* Anything in the session buffer is written into the new memory, so the part of
* the conversation that happened before the database existed is not lost.
* Shared by both entry points so neither can forget to do it.
*/
export async function attachAndBackfill(
uri: string,
scope: MemoryScope,
sessionBuffer: LlmMessage[],
log: (line: string) => void = console.log
): Promise<void> {
await attach(uri, log);
if (sessionBuffer.length === 0) return;
log(`Backfilling ${sessionBuffer.length} messages from this session...`);
for (const message of sessionBuffer) {
await remember(scope, message.role, message.content);
}
sessionBuffer.length = 0;
log("Backfill done. Everything you already said is now remembered.");
}
function systemPrompt(facts: string[], offerTool: boolean): string {
if (!isReady()) {
const amnesia = `${BASE_PROMPT}
You currently have no memory. You will forget this entire conversation when the process stops. Be honest about that if the person asks.`;
return offerTool
? `${amnesia}\n\nYou do have a way to fix it. If they explicitly ask to be remembered across sessions, use the provision_memory tool. Do not use it just because they share a useful fact.`
: amnesia;
}
const memoryStatus =
"\n\nPersistent memory is attached for this conversation. Durable facts the person shares can be remembered across sessions in this memory scope.";
if (facts.length === 0) return `${BASE_PROMPT}${memoryStatus}`;
return `${BASE_PROMPT}
${memoryStatus.trim()}
Here is what you remember about this person from earlier conversations:
${facts.map((fact) => `- ${fact}`).join("\n")}
Use these facts naturally. Do not list them back unless asked.`;
}