@aexhq/agentloop-pi 2.1.0 → 4.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 +21 -6
- package/dist/index.mjs +2 -0
- package/dist/loop.component.wasm +0 -0
- package/index.d.ts +4 -2
- package/package.json +3 -3
- package/src/component.mjs +3 -3
- package/src/index.mjs +2 -0
- package/src/logic.mjs +23 -8
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ pi's steering and follow-up queues and per-tool `executionMode` are host-app sea
|
|
|
17
17
|
equivalent and are not ported.
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import {
|
|
20
|
+
import { brainEnv } from "@aexhq/brain";
|
|
21
21
|
import { pi } from "@aexhq/agentloop-pi";
|
|
22
22
|
|
|
23
|
-
const loopRuntime =
|
|
23
|
+
const loopRuntime = brainEnv({ name: "brain" });
|
|
24
24
|
const session = await brain.sessions.create({
|
|
25
25
|
agentloop: pi({ env: loopRuntime, contextWindow: 200_000 }),
|
|
26
26
|
model,
|
|
@@ -32,8 +32,23 @@ The component is built by this package's publisher. Brain consumes the resulting
|
|
|
32
32
|
does not compile its JavaScript source.
|
|
33
33
|
|
|
34
34
|
The loop reads paginated session Events before each turn and saves its observation cursor in
|
|
35
|
-
|
|
35
|
+
kv. Interrupted turns and environment failures enter the transcript as runtime observations.
|
|
36
36
|
A failed tool result goes back to the model with `is_error`; the loop does not retry it automatically.
|
|
37
|
-
Brain can release execution between turns without losing this transcript or cursor.
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
Brain can release execution between turns without losing this transcript or cursor. The caller
|
|
38
|
+
controls Environment lifetime; a browser closure or provider resource loss can still destroy its
|
|
39
|
+
physical state.
|
|
40
|
+
|
|
41
|
+
By default the model sees canonical Tool schemas. A Tool with multiple placements requires an
|
|
42
|
+
explicit `placements: { toolName: "environmentName" }` option. Set
|
|
43
|
+
`environmentSelection: "model"` to expose each Tool's authorized Environment names in its input
|
|
44
|
+
schema. The loop unwraps that choice before dispatch; Brain validates the selected pair and the
|
|
45
|
+
canonical input. Both modes use the same session Tool placements.
|
|
46
|
+
|
|
47
|
+
Transcript and kv changes are saved inline through Brain's state services. A later model or
|
|
48
|
+
Tool failure preserves acknowledged writes; turn output contains only the result. Retained
|
|
49
|
+
native model blocks pass through unchanged, and user images enter model context as media.
|
|
50
|
+
Compaction explicitly resets the response format and installs a summary only after `end_turn`;
|
|
51
|
+
a truncated, refused, or unknown summary leaves the original saved context intact.
|
|
52
|
+
|
|
53
|
+
Rebuild Components against the matching Brain WIT when upgrading. Existing sessions keep their
|
|
54
|
+
immutable loop implementation and must be recreated for this interface change.
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { agentloop, component } from "@aexhq/brain";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
var options = z.object({
|
|
5
|
+
environmentSelection: z.enum(["hidden", "model"]).default("hidden"),
|
|
6
|
+
placements: z.record(z.string(), z.string()).default({}),
|
|
5
7
|
contextWindow: z.number().int().positive().default(2e5),
|
|
6
8
|
// pi defaults (compaction.ts): compact when context exceeds
|
|
7
9
|
// contextWindow - reserveTokens, keep ~keepRecentTokens of recent messages.
|
package/dist/loop.component.wasm
CHANGED
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PlacedAgentloop, Environment } from "@aexhq/brain";
|
|
2
2
|
|
|
3
3
|
export interface PiOptions {
|
|
4
4
|
readonly env: Environment;
|
|
5
|
+
readonly environmentSelection?: "hidden" | "model";
|
|
6
|
+
readonly placements?: Readonly<Record<string, string>>;
|
|
5
7
|
/** Model context window in tokens the compaction budget is measured against. Default 200000. */
|
|
6
8
|
readonly contextWindow?: number;
|
|
7
9
|
/** Compact when the estimated context exceeds contextWindow - reserveTokens. pi default 16384. */
|
|
@@ -12,4 +14,4 @@ export interface PiOptions {
|
|
|
12
14
|
readonly compaction?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export declare const pi: (options: PiOptions) =>
|
|
17
|
+
export declare const pi: (options: PiOptions) => PlacedAgentloop;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/agentloop-pi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.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": true,
|
|
19
19
|
"tag": "next"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepack": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@aexhq/brain": "0.
|
|
37
|
+
"@aexhq/brain": "0.19.0",
|
|
38
38
|
"zod": "4.4.3"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {}
|
package/src/component.mjs
CHANGED
|
@@ -19,12 +19,14 @@ export async function turn(input) {
|
|
|
19
19
|
const output = await runPi({
|
|
20
20
|
input: JSON.parse(input.inputJson),
|
|
21
21
|
transcript: JSON.parse(input.transcriptJson),
|
|
22
|
-
|
|
22
|
+
kv: JSON.parse(input.kvJson),
|
|
23
23
|
events: JSON.parse(input.eventsJson),
|
|
24
24
|
configuration: JSON.parse(input.configurationJson),
|
|
25
25
|
system: input.system,
|
|
26
26
|
tools: JSON.parse(input.toolsJson),
|
|
27
27
|
}, {
|
|
28
|
+
setTranscript: (messages) => hosted(() => host.setTranscript(JSON.stringify(messages))),
|
|
29
|
+
setKv: (key, value) => hosted(() => host.setKv(key, JSON.stringify(value))),
|
|
28
30
|
events: (after) => JSON.parse(hosted(() => host.events(BigInt(after)))),
|
|
29
31
|
model: (request) => JSON.parse(hosted(() => host.model(JSON.stringify(request)))),
|
|
30
32
|
dispatch: (calls) => JSON.parse(hosted(() => host.dispatch(JSON.stringify(calls)))),
|
|
@@ -32,8 +34,6 @@ export async function turn(input) {
|
|
|
32
34
|
telemetry: (record) => host.telemetry(JSON.stringify(record ?? null)),
|
|
33
35
|
});
|
|
34
36
|
return {
|
|
35
|
-
transcriptJson: JSON.stringify(output.transcript),
|
|
36
|
-
slotsJson: JSON.stringify(output.slots),
|
|
37
37
|
resultJson: output.result === undefined ? undefined : JSON.stringify(output.result),
|
|
38
38
|
};
|
|
39
39
|
} catch (error) {
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { agentloop, component } from "@aexhq/brain";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
const options = z.object({
|
|
4
|
+
environmentSelection: z.enum(["hidden", "model"]).default("hidden"),
|
|
5
|
+
placements: z.record(z.string(), z.string()).default({}),
|
|
4
6
|
contextWindow: z.number().int().positive().default(200_000),
|
|
5
7
|
// pi defaults (compaction.ts): compact when context exceeds
|
|
6
8
|
// contextWindow - reserveTokens, keep ~keepRecentTokens of recent messages.
|
package/src/logic.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toolPlacement } from "../../../shared/tool-placement.mjs";
|
|
1
2
|
import { observeEvents } from "../../../shared/loop-events.mjs";
|
|
2
3
|
|
|
3
4
|
const CHECKPOINT_PREFIX = "Context checkpoint from earlier in this conversation:\n\n";
|
|
@@ -44,6 +45,8 @@ const text = (message) => message.content.filter((block) => block.type === "text
|
|
|
44
45
|
const blockText = (block) => {
|
|
45
46
|
if (block.type === "text") return block.text;
|
|
46
47
|
if (block.type === "tool_use") return `[tool_use ${block.name}] ${JSON.stringify(block.input)}`;
|
|
48
|
+
if (block.type === "native") return `[provider continuation state: ${block.format}]`;
|
|
49
|
+
if (block.type === "image") return block.url.startsWith("https://") ? `[image ${block.url}]` : "[embedded image]";
|
|
47
50
|
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
48
51
|
return `[tool_result${block.is_error ? " (error)" : ""}] ${output.length > 2000 ? `${output.slice(0, 2000)}…` : output}`;
|
|
49
52
|
};
|
|
@@ -59,9 +62,11 @@ export async function runPi(input, context) {
|
|
|
59
62
|
compaction: true,
|
|
60
63
|
...input.configuration,
|
|
61
64
|
};
|
|
65
|
+
const placement = toolPlacement(input.tools, options);
|
|
62
66
|
const transcript = cloneJson(input.transcript);
|
|
63
|
-
const observed_sequence = await observeEvents(context, transcript, input.
|
|
64
|
-
const saved = input.
|
|
67
|
+
const observed_sequence = await observeEvents(context, transcript, input.kv.observed_sequence ?? 0);
|
|
68
|
+
const saved = input.kv.checkpoint;
|
|
69
|
+
await context.setKv("observed_sequence", observed_sequence);
|
|
65
70
|
const checkpoint = saved === undefined ? { summary: null } : cloneJson(saved);
|
|
66
71
|
const body = () => checkpoint.summary === null ? transcript : transcript.slice(1);
|
|
67
72
|
const shouldCompact = () =>
|
|
@@ -85,26 +90,36 @@ export async function runPi(input, context) {
|
|
|
85
90
|
if (cut === 0) return;
|
|
86
91
|
const previous = checkpoint.summary === null ? "" : `Previous summary:\n\n${checkpoint.summary}\n\n`;
|
|
87
92
|
const prompt = checkpoint.summary === null ? SUMMARIZATION_PROMPT : `${UPDATE_RULES}${SUMMARIZATION_PROMPT}`;
|
|
88
|
-
const { message } = await context.model({
|
|
93
|
+
const { message, stop_reason } = await context.model({
|
|
94
|
+
response_format: null,
|
|
95
|
+
tools: [],
|
|
89
96
|
messages: [{ role: "user", content: [{ type: "text", text: `${previous}${serializeConversation(messages.slice(0, cut))}\n\n${prompt}` }] }],
|
|
90
97
|
});
|
|
98
|
+
if (stop_reason !== "end_turn") throw new Error(`Compaction did not complete: ${stop_reason}`);
|
|
91
99
|
checkpoint.summary = text(message);
|
|
92
100
|
transcript.splice(0, transcript.length,
|
|
93
101
|
{ role: "user", content: [{ type: "text", text: `${CHECKPOINT_PREFIX}${checkpoint.summary}` }] },
|
|
94
102
|
...messages.slice(cut));
|
|
95
103
|
};
|
|
96
104
|
|
|
97
|
-
transcript.push({ role: "user", content: [{ type: "text", text: input.input.message }] });
|
|
105
|
+
transcript.push({ role: "user", content: [{ type: "text", text: input.input.message }, ...(input.input.media ?? [])] });
|
|
98
106
|
for (;;) {
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
await context.setTranscript(transcript);
|
|
108
|
+
if (shouldCompact()) {
|
|
109
|
+
await compact();
|
|
110
|
+
await context.setTranscript(transcript);
|
|
111
|
+
await context.setKv("checkpoint", checkpoint);
|
|
112
|
+
}
|
|
113
|
+
const { message, stop_reason } = await context.model({ messages: transcript, tools: placement.definitions });
|
|
101
114
|
transcript.push(message);
|
|
115
|
+
await context.setTranscript(transcript);
|
|
116
|
+
await context.setKv("checkpoint", checkpoint);
|
|
102
117
|
const calls = message.content
|
|
103
118
|
.filter((block) => block.type === "tool_use")
|
|
104
119
|
.map((block) => ({ call_id: block.id, name: block.name, input: block.input }));
|
|
105
120
|
if (calls.length === 0) {
|
|
106
121
|
await context.emit("output_emitted", { type: "assistant_message", message: text(message) });
|
|
107
|
-
return {
|
|
122
|
+
return {};
|
|
108
123
|
}
|
|
109
124
|
if (stop_reason === "max_tokens") {
|
|
110
125
|
transcript.push({
|
|
@@ -113,7 +128,7 @@ export async function runPi(input, context) {
|
|
|
113
128
|
});
|
|
114
129
|
continue;
|
|
115
130
|
}
|
|
116
|
-
const results = await context.dispatch(calls);
|
|
131
|
+
const results = await context.dispatch(calls.map(placement.invocation));
|
|
117
132
|
const byCall = new Map(results.map((result) => [result.call_id, result]));
|
|
118
133
|
transcript.push({
|
|
119
134
|
role: "user",
|