Repository navigation
Expand file tree
/
Copy path09-context.ts
More file actions
136 lines (125 loc) · 4.83 KB
/
Copy path09-context.ts
File metadata and controls
136 lines (125 loc) · 4.83 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
// Transcript history and model context.
// Run from packages/durable:
// node --conditions=source --experimental-strip-types test/examples/09-context.ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import type { AssistantMessage, Message, StopReason, ToolResultMessage } from "@earendil-works/pi-ai";
import { createModels } from "@earendil-works/pi-ai/models";
import { createRegistry, Harness, MemoryStorage } from "../../src/index.ts";
const context = BACKGROUND_CONTEXT;
const harness = await Harness.open(
new MemoryStorage(),
{ models: createModels(), registry: createRegistry() },
context,
);
// Entries are immutable. `model` holds the messages an entry contributes to
// the next model request; `data` is for the app only. context() turns the
// stored transcript into those request messages:
// - an entry with `head` starts a new context; older entries stay stored,
// - `edits` replace or omit what an earlier entry contributes,
// - aborted, error, and deferred assistant messages are not sent,
// - tool results are sent right after their call, in call order,
// - a call without a result gets a synthesized error result.
function assistantMessage(text: string, calls: readonly string[] = [], stopReason?: StopReason): AssistantMessage {
return {
role: "assistant",
content: [
{ type: "text", text },
...calls.map((id) => ({ type: "toolCall" as const, id, name: "read", arguments: {} })),
],
api: "example",
provider: "example",
model: "example",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: stopReason ?? (calls.length > 0 ? "toolUse" : "stop"),
timestamp: 2,
};
}
function toolResultMessage(id: string): ToolResultMessage {
return {
role: "toolResult",
toolCallId: id,
toolName: "read",
content: [{ type: "text", text: `file ${id}` }],
isError: false,
timestamp: 3,
};
}
function show(message: Message): string {
switch (message.role) {
case "user":
return `user: ${message.content as string}`;
case "system":
return `system: ${JSON.stringify(message.sections)}`;
case "assistant":
return `assistant: ${message.content
.map((part) => (part.type === "text" ? part.text : part.type === "toolCall" ? `call(${part.id})` : ""))
.join(" ")}`;
case "toolResult":
return `result(${message.toolCallId})${message.isError ? " error" : ""}`;
}
}
const transcript = await harness.createConversation({ ownership: { kind: "ownerless" } }, context);
const say = (kind: string, ...model: Message[]) =>
transcript.commit((tx) => tx.appendEntry(transcript.id, { kind, model }), context);
const question = await say("message", { role: "user", content: "read a and b", timestamp: 1 });
await say("message", assistantMessage("I crashed", [], "aborted")); // stored, never sent
const calls = await say("message", assistantMessage("reading", ["a", "b"]));
await say("message", toolResultMessage("b")); // results finish out of order
await say("pi.system", { role: "system", content: "", sections: { cwd: "<cwd>/repo</cwd>" }, timestamp: 4 });
await say("message", toolResultMessage("a"));
await say("message", assistantMessage("a and b look fine"));
await transcript.commit(
(tx) =>
tx.appendEntry(transcript.id, {
kind: "edit",
data: "user fixed a typo",
edits: [
{
target: question.id,
action: "replace",
messages: [{ role: "user", content: "read files a and b", timestamp: 1 }],
},
],
}),
context,
);
await transcript.commit((tx) => tx.appendEntry(transcript.id, { kind: "note", data: "display only" }), context);
let view = await transcript.context(context);
console.log(
"raw active entries:",
view.entries.map((entry) => entry.kind),
);
console.log("request messages:", view.messages.map(show));
// A fork at the tool call has no results yet; context() fills them in.
const cut = await transcript.fork(calls.id, { ownership: { kind: "ownerless" } }, context);
console.log("fork messages:", (await cut.context(context)).messages.map(show));
// A headed summary replaces everything before the entry it points at.
// "self" points the head at the summary entry itself.
await transcript.commit(
(tx) =>
tx.appendEntry(transcript.id, {
kind: "summary",
head: "self",
model: [{ role: "user", content: "Summary: a and b are fine.", timestamp: 5 }],
}),
context,
);
view = await transcript.context(context);
console.log("after summary:", view.head?.kind, view.messages.map(show));
// entries() pages the stored transcript, newest first, including inherited
// parent entries. Nothing is ever deleted by heads or edits.
const history = await transcript.entries({}, 3, undefined, context);
console.log(
"newest stored entries:",
history.items.map((entry) => entry.kind),
"more:",
history.next !== undefined,
);
await harness.close(context);