@aexhq/agentloop-pi 1.6.0 → 2.0.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/README.md +15 -12
- package/dist/index.mjs +18 -5
- package/dist/loop.component.wasm +0 -0
- package/index.d.ts +3 -2
- package/package.json +4 -4
- package/src/component.mjs +47 -0
- package/src/index.mjs +6 -180
- package/src/logic.mjs +130 -0
- package/dist/pi.brain.json +0 -1
- package/dist/source.mjs +0 -14667
package/README.md
CHANGED
|
@@ -2,28 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
A pi-style agent loop for Brain: a semantic port of the pi coding agent's loop,
|
|
4
4
|
pinned against [earendil-works/pi](https://github.com/earendil-works/pi) tag
|
|
5
|
-
`v0.84.4` (`@earendil-works/pi-agent-core@0.84.4`). The
|
|
6
|
-
through Brain's
|
|
5
|
+
`v0.84.4` (`@earendil-works/pi-agent-core@0.84.4`). The package ships a precompiled
|
|
6
|
+
WebAssembly Component that drives each turn through Brain's Agentloop host imports and reproduces
|
|
7
7
|
pi's per-turn contract:
|
|
8
8
|
|
|
9
|
-
- tool calls are issued as one **parallel batch**, and results return in
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
without executing it** and re-asks the model;
|
|
9
|
+
- tool calls are issued as one **parallel batch**, and results return in assistant source order;
|
|
10
|
+
- a `length`-stopped response that carries tool calls **fails the whole batch without executing
|
|
11
|
+
it** and re-asks the model;
|
|
13
12
|
- **automatic compaction**: when the estimated context exceeds
|
|
14
|
-
`contextWindow - reserveTokens` (default 16384), history older than
|
|
15
|
-
|
|
16
|
-
context checkpoint (`## Goal` … `## Critical Context`) that replaces it.
|
|
13
|
+
`contextWindow - reserveTokens` (default 16384), history older than ~`keepRecentTokens`
|
|
14
|
+
(default 20000) is summarized into pi's structured context checkpoint.
|
|
17
15
|
|
|
18
|
-
pi's steering and follow-up queues and per-tool `executionMode` are host-app
|
|
19
|
-
|
|
16
|
+
pi's steering and follow-up queues and per-tool `executionMode` are host-app seams with no Brain
|
|
17
|
+
equivalent and are not ported.
|
|
20
18
|
|
|
21
19
|
```ts
|
|
20
|
+
import { brainWasm } from "@aexhq/brain";
|
|
22
21
|
import { pi } from "@aexhq/agentloop-pi";
|
|
23
22
|
|
|
23
|
+
const loopRuntime = brainWasm();
|
|
24
24
|
const session = await brain.sessions.create({
|
|
25
|
-
agentloop: pi({ contextWindow: 200_000 }),
|
|
25
|
+
agentloop: pi({ env: loopRuntime, contextWindow: 200_000 }),
|
|
26
26
|
model,
|
|
27
27
|
tools: [read({ env: workspace })],
|
|
28
28
|
});
|
|
29
29
|
```
|
|
30
|
+
|
|
31
|
+
The component is built by this package's publisher. Brain consumes the resulting Component and
|
|
32
|
+
does not compile its JavaScript source.
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// src/index.mjs
|
|
2
|
+
import { agentloop, component } from "@aexhq/brain";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
var options = z.object({
|
|
5
|
+
contextWindow: z.number().int().positive().default(2e5),
|
|
6
|
+
// pi defaults (compaction.ts): compact when context exceeds
|
|
7
|
+
// contextWindow - reserveTokens, keep ~keepRecentTokens of recent messages.
|
|
8
|
+
reserveTokens: z.number().int().positive().default(16384),
|
|
9
|
+
keepRecentTokens: z.number().int().positive().default(2e4),
|
|
10
|
+
compaction: z.boolean().default(true)
|
|
11
|
+
}).strict();
|
|
12
|
+
var pi = agentloop({
|
|
13
|
+
options,
|
|
14
|
+
implementation: component(new URL("./loop.component.wasm", import.meta.url))
|
|
15
|
+
});
|
|
16
|
+
export {
|
|
17
|
+
pi
|
|
18
|
+
};
|
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentloopBinding, Environment } from "@aexhq/brain";
|
|
2
2
|
|
|
3
3
|
export interface PiOptions {
|
|
4
|
+
readonly env: Environment;
|
|
4
5
|
/** Model context window in tokens the compaction budget is measured against. Default 200000. */
|
|
5
6
|
readonly contextWindow?: number;
|
|
6
7
|
/** Compact when the estimated context exceeds contextWindow - reserveTokens. pi default 16384. */
|
|
@@ -11,4 +12,4 @@ export interface PiOptions {
|
|
|
11
12
|
readonly compaction?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export declare const pi: (options
|
|
15
|
+
export declare const pi: (options: PiOptions) => AgentloopBinding;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/agentloop-pi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Pi-style parallel-Tool Brain extension",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public",
|
|
18
|
-
"provenance":
|
|
18
|
+
"provenance": false,
|
|
19
19
|
"tag": "next"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "
|
|
32
|
+
"build": "node ../../tools/build-agentloop.mjs src/component.mjs src/index.mjs dist",
|
|
33
33
|
"test": "npm run build && node --test",
|
|
34
34
|
"prepack": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@aexhq/brain": "0.
|
|
37
|
+
"@aexhq/brain": "0.16.0",
|
|
38
38
|
"zod": "4.4.3"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as host from "brain:agentloop/host@0.1.0";
|
|
2
|
+
|
|
3
|
+
import { runPi } from "./logic.mjs";
|
|
4
|
+
|
|
5
|
+
function hosted(call) {
|
|
6
|
+
try {
|
|
7
|
+
return call();
|
|
8
|
+
} catch (error) {
|
|
9
|
+
const payload = error !== null && typeof error === "object" && "payload" in error ? error.payload : undefined;
|
|
10
|
+
const failure = new Error(payload?.message ?? String(error?.message ?? error));
|
|
11
|
+
failure.code = payload?.code ?? "host_error";
|
|
12
|
+
failure.retryable = payload?.retryable ?? false;
|
|
13
|
+
throw failure;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function turn(input) {
|
|
18
|
+
try {
|
|
19
|
+
const output = await runPi({
|
|
20
|
+
input: JSON.parse(input.inputJson),
|
|
21
|
+
transcript: JSON.parse(input.transcriptJson),
|
|
22
|
+
slots: JSON.parse(input.slotsJson),
|
|
23
|
+
events: JSON.parse(input.eventsJson),
|
|
24
|
+
configuration: JSON.parse(input.configurationJson),
|
|
25
|
+
system: input.system,
|
|
26
|
+
tools: JSON.parse(input.toolsJson),
|
|
27
|
+
}, {
|
|
28
|
+
model: (request) => JSON.parse(hosted(() => host.model(JSON.stringify(request)))),
|
|
29
|
+
dispatch: (calls) => JSON.parse(hosted(() => host.dispatch(JSON.stringify(calls)))),
|
|
30
|
+
emit: (kind, data) => hosted(() => host.emit(kind, JSON.stringify(data ?? null))),
|
|
31
|
+
telemetry: (record) => host.telemetry(JSON.stringify(record ?? null)),
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
transcriptJson: JSON.stringify(output.transcript),
|
|
35
|
+
slotsJson: JSON.stringify(output.slots),
|
|
36
|
+
resultJson: output.result === undefined ? undefined : JSON.stringify(output.result),
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const failure = new Error(String(error?.message ?? error) || "Agentloop turn failed");
|
|
40
|
+
failure.payload = {
|
|
41
|
+
code: typeof error?.code === "string" && error.code.length > 0 ? error.code : "agentloop_failed",
|
|
42
|
+
message: failure.message,
|
|
43
|
+
retryable: Boolean(error?.retryable),
|
|
44
|
+
};
|
|
45
|
+
throw failure;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -1,189 +1,15 @@
|
|
|
1
|
-
import { agentloop } from "@aexhq/brain";
|
|
1
|
+
import { agentloop, component } from "@aexhq/brain";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
// Semantic port of the pi coding agent's loop, pinned against
|
|
5
|
-
// earendil-works/pi tag v0.84.4 (@earendil-works/pi-agent-core@0.84.4,
|
|
6
|
-
// packages/agent/src/agent-loop.ts + packages/coding-agent/src/core/compaction/compaction.ts).
|
|
7
|
-
//
|
|
8
|
-
// The loop drives one whole turn through Brain's services: it calls the model,
|
|
9
|
-
// hands tool calls to Brain, and edits the transcript Brain persists. What it
|
|
10
|
-
// reproduces of pi:
|
|
11
|
-
// - tool calls are issued as one parallel batch, results return as one
|
|
12
|
-
// user message of tool_result blocks in assistant source order
|
|
13
|
-
// - a length-stop that carries tool calls fails the whole batch without
|
|
14
|
-
// executing it, and the loop re-asks the model
|
|
15
|
-
// - automatic compaction: when the estimated context exceeds
|
|
16
|
-
// contextWindow - reserveTokens, everything older than ~keepRecentTokens
|
|
17
|
-
// is summarized into a structured context checkpoint that replaces it
|
|
18
|
-
// (cut points never split a tool_result away from its tool_use)
|
|
19
|
-
// Host-app seams of pi (steering and follow-up queues, per-tool
|
|
20
|
-
// executionMode) have no Brain equivalent and are not ported.
|
|
21
|
-
|
|
22
|
-
const optionsSchema = z.object({
|
|
3
|
+
const options = z.object({
|
|
23
4
|
contextWindow: z.number().int().positive().default(200_000),
|
|
24
5
|
// pi defaults (compaction.ts): compact when context exceeds
|
|
25
6
|
// contextWindow - reserveTokens, keep ~keepRecentTokens of recent messages.
|
|
26
7
|
reserveTokens: z.number().int().positive().default(16_384),
|
|
27
8
|
keepRecentTokens: z.number().int().positive().default(20_000),
|
|
28
9
|
compaction: z.boolean().default(true),
|
|
29
|
-
}).
|
|
30
|
-
|
|
31
|
-
// The checkpoint is the transcript's first message once a compaction has
|
|
32
|
-
// happened; the slot remembers its text so the next compaction can update it.
|
|
33
|
-
const checkpointSchema = z.object({ summary: z.union([z.string(), z.null()]) });
|
|
34
|
-
|
|
35
|
-
const CHECKPOINT_PREFIX = "Context checkpoint from earlier in this conversation:\n\n";
|
|
36
|
-
|
|
37
|
-
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
|
|
38
|
-
|
|
39
|
-
Use this EXACT format:
|
|
40
|
-
|
|
41
|
-
## Goal
|
|
42
|
-
|
|
43
|
-
## Constraints & Preferences
|
|
44
|
-
|
|
45
|
-
## Progress
|
|
46
|
-
|
|
47
|
-
### Done
|
|
48
|
-
|
|
49
|
-
### In Progress
|
|
50
|
-
|
|
51
|
-
### Blocked
|
|
52
|
-
|
|
53
|
-
## Key Decisions
|
|
54
|
-
|
|
55
|
-
## Next Steps
|
|
56
|
-
|
|
57
|
-
## Critical Context
|
|
58
|
-
|
|
59
|
-
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
60
|
-
|
|
61
|
-
const UPDATE_RULES = `Update the existing structured summary with new information.
|
|
62
|
-
|
|
63
|
-
RULES:
|
|
64
|
-
- PRESERVE all existing information from the previous summary
|
|
65
|
-
- ADD new progress, decisions, and context from the new messages
|
|
66
|
-
- Move items from In Progress to Done as they complete
|
|
67
|
-
|
|
68
|
-
`;
|
|
69
|
-
|
|
70
|
-
const TRUNCATED_CALL_MESSAGE =
|
|
71
|
-
"Tool call was not executed: the model response was cut off by the output token limit, so the arguments may be truncated. Re-issue the tool call.";
|
|
72
|
-
|
|
73
|
-
const text = (message) =>
|
|
74
|
-
message.content
|
|
75
|
-
.filter((block) => block.type === "text")
|
|
76
|
-
.map((block) => block.text)
|
|
77
|
-
.join("");
|
|
78
|
-
|
|
79
|
-
const blockText = (block) => {
|
|
80
|
-
if (block.type === "text") return block.text;
|
|
81
|
-
if (block.type === "tool_use") return `[tool_use ${block.name}] ${JSON.stringify(block.input)}`;
|
|
82
|
-
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
83
|
-
return `[tool_result${block.is_error ? " (error)" : ""}] ${output.length > 2000 ? `${output.slice(0, 2000)}…` : output}`;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const serializeConversation = (messages) =>
|
|
87
|
-
messages.map((message) => `${message.role}:\n${message.content.map(blockText).join("\n")}`).join("\n\n");
|
|
88
|
-
|
|
89
|
-
export const pi = agentloop({ options: optionsSchema }, (author) => {
|
|
90
|
-
const options = author.options;
|
|
91
|
-
const checkpoint = author.slot("checkpoint", checkpointSchema, () => ({ summary: null }));
|
|
92
|
-
|
|
93
|
-
// The conversation proper: the transcript minus the checkpoint message.
|
|
94
|
-
const body = (transcript) => (checkpoint.summary === null ? transcript : transcript.slice(1));
|
|
95
|
-
|
|
96
|
-
const shouldCompact = (transcript) =>
|
|
97
|
-
options.compaction && author.context.estimateTokens(transcript) > options.contextWindow - options.reserveTokens;
|
|
98
|
-
|
|
99
|
-
// Earliest index the recent-token budget allows, then forward to the next
|
|
100
|
-
// boundary that does not split a tool_result away from its tool_use.
|
|
101
|
-
const cutPoint = (messages) => {
|
|
102
|
-
let kept = 0;
|
|
103
|
-
let cut = 0;
|
|
104
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
105
|
-
kept += author.context.estimateTokens([messages[index]]);
|
|
106
|
-
if (kept > options.keepRecentTokens) {
|
|
107
|
-
cut = index + 1;
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
while (
|
|
112
|
-
cut < messages.length &&
|
|
113
|
-
messages[cut].role === "user" &&
|
|
114
|
-
messages[cut].content.some((block) => block.type === "tool_result")
|
|
115
|
-
) {
|
|
116
|
-
cut += 1;
|
|
117
|
-
}
|
|
118
|
-
return cut;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const compact = async (turn) => {
|
|
122
|
-
const messages = body(turn.transcript);
|
|
123
|
-
const cut = cutPoint(messages);
|
|
124
|
-
if (cut === 0) return;
|
|
125
|
-
const previous = checkpoint.summary === null ? "" : `Previous summary:\n\n${checkpoint.summary}\n\n`;
|
|
126
|
-
const prompt = checkpoint.summary === null ? SUMMARIZATION_PROMPT : `${UPDATE_RULES}${SUMMARIZATION_PROMPT}`;
|
|
127
|
-
const { message } = await turn.model({
|
|
128
|
-
messages: [
|
|
129
|
-
{
|
|
130
|
-
role: "user",
|
|
131
|
-
content: [{ type: "text", text: `${previous}${serializeConversation(messages.slice(0, cut))}\n\n${prompt}` }],
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
});
|
|
135
|
-
checkpoint.summary = text(message);
|
|
136
|
-
turn.transcript.splice(
|
|
137
|
-
0,
|
|
138
|
-
turn.transcript.length,
|
|
139
|
-
{ role: "user", content: [{ type: "text", text: `${CHECKPOINT_PREFIX}${checkpoint.summary}` }] },
|
|
140
|
-
...messages.slice(cut),
|
|
141
|
-
);
|
|
142
|
-
};
|
|
10
|
+
}).strict();
|
|
143
11
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (shouldCompact(turn.transcript)) await compact(turn);
|
|
148
|
-
const { message, stop_reason } = await turn.model({ messages: turn.transcript });
|
|
149
|
-
turn.transcript.push(message);
|
|
150
|
-
const calls = message.content
|
|
151
|
-
.filter((block) => block.type === "tool_use")
|
|
152
|
-
.map((block) => ({ callId: block.id, name: block.name, input: block.input }));
|
|
153
|
-
if (calls.length === 0) {
|
|
154
|
-
await turn.reply(text(message));
|
|
155
|
-
return turn.done();
|
|
156
|
-
}
|
|
157
|
-
if (stop_reason === "max_tokens") {
|
|
158
|
-
// pi fails the whole batch without executing it when the response was
|
|
159
|
-
// cut off: the arguments cannot be trusted.
|
|
160
|
-
turn.transcript.push({
|
|
161
|
-
role: "user",
|
|
162
|
-
content: calls.map((call) => ({
|
|
163
|
-
type: "tool_result",
|
|
164
|
-
tool_use_id: call.callId,
|
|
165
|
-
content: TRUNCATED_CALL_MESSAGE,
|
|
166
|
-
is_error: true,
|
|
167
|
-
})),
|
|
168
|
-
});
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
// One dispatch: Brain runs the batch in parallel and reports back once.
|
|
172
|
-
const results = await turn.dispatch(calls);
|
|
173
|
-
const byCall = new Map(results.map((result) => [result.callId, result]));
|
|
174
|
-
turn.transcript.push({
|
|
175
|
-
role: "user",
|
|
176
|
-
// Assistant source order, the order pi appends result messages in.
|
|
177
|
-
content: calls.map(({ callId }) => {
|
|
178
|
-
const result = byCall.get(callId);
|
|
179
|
-
return {
|
|
180
|
-
type: "tool_result",
|
|
181
|
-
tool_use_id: callId,
|
|
182
|
-
content: result === undefined ? "Tool produced no result." : result.output,
|
|
183
|
-
is_error: result === undefined ? true : result.isError,
|
|
184
|
-
};
|
|
185
|
-
}),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
});
|
|
12
|
+
export const pi = agentloop({
|
|
13
|
+
options,
|
|
14
|
+
implementation: component(new URL("./loop.component.wasm", import.meta.url)),
|
|
189
15
|
});
|
package/src/logic.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const CHECKPOINT_PREFIX = "Context checkpoint from earlier in this conversation:\n\n";
|
|
2
|
+
|
|
3
|
+
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
|
|
4
|
+
|
|
5
|
+
Use this EXACT format:
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
|
|
9
|
+
## Constraints & Preferences
|
|
10
|
+
|
|
11
|
+
## Progress
|
|
12
|
+
|
|
13
|
+
### Done
|
|
14
|
+
|
|
15
|
+
### In Progress
|
|
16
|
+
|
|
17
|
+
### Blocked
|
|
18
|
+
|
|
19
|
+
## Key Decisions
|
|
20
|
+
|
|
21
|
+
## Next Steps
|
|
22
|
+
|
|
23
|
+
## Critical Context
|
|
24
|
+
|
|
25
|
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
26
|
+
|
|
27
|
+
const UPDATE_RULES = `Update the existing structured summary with new information.
|
|
28
|
+
|
|
29
|
+
RULES:
|
|
30
|
+
- PRESERVE all existing information from the previous summary
|
|
31
|
+
- ADD new progress, decisions, and context from the new messages
|
|
32
|
+
- Move items from In Progress to Done as they complete
|
|
33
|
+
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const TRUNCATED_CALL_MESSAGE =
|
|
37
|
+
"Tool call was not executed: the model response was cut off by the output token limit, so the arguments may be truncated. Re-issue the tool call.";
|
|
38
|
+
|
|
39
|
+
const estimateTokens = (messages) => Math.ceil(JSON.stringify(messages).length / 4);
|
|
40
|
+
const text = (message) => message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
41
|
+
|
|
42
|
+
const blockText = (block) => {
|
|
43
|
+
if (block.type === "text") return block.text;
|
|
44
|
+
if (block.type === "tool_use") return `[tool_use ${block.name}] ${JSON.stringify(block.input)}`;
|
|
45
|
+
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
46
|
+
return `[tool_result${block.is_error ? " (error)" : ""}] ${output.length > 2000 ? `${output.slice(0, 2000)}…` : output}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const serializeConversation = (messages) =>
|
|
50
|
+
messages.map((message) => `${message.role}:\n${message.content.map(blockText).join("\n")}`).join("\n\n");
|
|
51
|
+
|
|
52
|
+
export async function runPi(input, context) {
|
|
53
|
+
const options = {
|
|
54
|
+
contextWindow: 200_000,
|
|
55
|
+
reserveTokens: 16_384,
|
|
56
|
+
keepRecentTokens: 20_000,
|
|
57
|
+
compaction: true,
|
|
58
|
+
...input.configuration,
|
|
59
|
+
};
|
|
60
|
+
const transcript = cloneJson(input.transcript);
|
|
61
|
+
const saved = input.slots.checkpoint;
|
|
62
|
+
const checkpoint = saved === undefined ? { summary: null } : cloneJson(saved);
|
|
63
|
+
const body = () => checkpoint.summary === null ? transcript : transcript.slice(1);
|
|
64
|
+
const shouldCompact = () =>
|
|
65
|
+
options.compaction && estimateTokens(transcript) > options.contextWindow - options.reserveTokens;
|
|
66
|
+
const cutPoint = (messages) => {
|
|
67
|
+
let kept = 0;
|
|
68
|
+
let cut = 0;
|
|
69
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
70
|
+
kept += estimateTokens([messages[index]]);
|
|
71
|
+
if (kept > options.keepRecentTokens) {
|
|
72
|
+
cut = index + 1;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
while (cut < messages.length && messages[cut].role === "user" && messages[cut].content.some((block) => block.type === "tool_result")) cut += 1;
|
|
77
|
+
return cut;
|
|
78
|
+
};
|
|
79
|
+
const compact = async () => {
|
|
80
|
+
const messages = body();
|
|
81
|
+
const cut = cutPoint(messages);
|
|
82
|
+
if (cut === 0) return;
|
|
83
|
+
const previous = checkpoint.summary === null ? "" : `Previous summary:\n\n${checkpoint.summary}\n\n`;
|
|
84
|
+
const prompt = checkpoint.summary === null ? SUMMARIZATION_PROMPT : `${UPDATE_RULES}${SUMMARIZATION_PROMPT}`;
|
|
85
|
+
const { message } = await context.model({
|
|
86
|
+
messages: [{ role: "user", content: [{ type: "text", text: `${previous}${serializeConversation(messages.slice(0, cut))}\n\n${prompt}` }] }],
|
|
87
|
+
});
|
|
88
|
+
checkpoint.summary = text(message);
|
|
89
|
+
transcript.splice(0, transcript.length,
|
|
90
|
+
{ role: "user", content: [{ type: "text", text: `${CHECKPOINT_PREFIX}${checkpoint.summary}` }] },
|
|
91
|
+
...messages.slice(cut));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
transcript.push({ role: "user", content: [{ type: "text", text: input.input.message }] });
|
|
95
|
+
for (;;) {
|
|
96
|
+
if (shouldCompact()) await compact();
|
|
97
|
+
const { message, stop_reason } = await context.model({ messages: transcript });
|
|
98
|
+
transcript.push(message);
|
|
99
|
+
const calls = message.content
|
|
100
|
+
.filter((block) => block.type === "tool_use")
|
|
101
|
+
.map((block) => ({ call_id: block.id, name: block.name, input: block.input }));
|
|
102
|
+
if (calls.length === 0) {
|
|
103
|
+
await context.emit("output_emitted", { type: "assistant_message", message: text(message) });
|
|
104
|
+
return { transcript, slots: { checkpoint } };
|
|
105
|
+
}
|
|
106
|
+
if (stop_reason === "max_tokens") {
|
|
107
|
+
transcript.push({
|
|
108
|
+
role: "user",
|
|
109
|
+
content: calls.map((call) => ({ type: "tool_result", tool_use_id: call.call_id, content: TRUNCATED_CALL_MESSAGE, is_error: true })),
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const results = await context.dispatch(calls);
|
|
114
|
+
const byCall = new Map(results.map((result) => [result.call_id, result]));
|
|
115
|
+
transcript.push({
|
|
116
|
+
role: "user",
|
|
117
|
+
content: calls.map(({ call_id }) => {
|
|
118
|
+
const result = byCall.get(call_id);
|
|
119
|
+
return {
|
|
120
|
+
type: "tool_result",
|
|
121
|
+
tool_use_id: call_id,
|
|
122
|
+
content: result === undefined ? "Tool produced no result." : result.output,
|
|
123
|
+
is_error: result === undefined ? true : result.is_error,
|
|
124
|
+
};
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
|