@aexhq/agentloop-pi 3.0.0 → 5.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 CHANGED
@@ -43,3 +43,15 @@ explicit `placements: { toolName: "environmentName" }` option. Set
43
43
  `environmentSelection: "model"` to expose each Tool's authorized Environment names in its input
44
44
  schema. The loop unwraps that choice before dispatch; Brain validates the selected pair and the
45
45
  canonical input. Both modes use the same session Tool placements.
46
+
47
+ Transcript changes and `ctx.kv.read/put/delete` use Brain's state services. KV mutations are
48
+ committed inline; missing keys remain distinct from stored JSON null. A later model or
49
+ Tool failure preserves acknowledged writes; turn output contains only the result. Retained
50
+ native model blocks pass through unchanged, and user images enter model context as media.
51
+ Compaction explicitly resets the response format and installs a summary only after `end_turn`;
52
+ a truncated, refused, or unknown summary leaves the original saved context intact.
53
+
54
+ Rebuild Components against the matching Brain WIT when upgrading. Existing sessions keep their
55
+ immutable loop implementation. Keep their matching server/artifacts for recovery and create
56
+ new sessions explicitly when adopting Brain 0.20 / version 5 extensions; do not delete old data
57
+ as an implicit upgrade step.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/agentloop-pi",
3
- "version": "3.0.0",
3
+ "version": "5.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": false,
18
+ "provenance": true,
19
19
  "tag": "next"
20
20
  },
21
21
  "exports": {
@@ -30,11 +30,12 @@
30
30
  ],
31
31
  "scripts": {
32
32
  "build": "node ../../tools/build-agentloop.mjs src/component.mjs src/index.mjs dist",
33
- "test": "npm run build && node --test",
33
+ "test": "npm run build && node --test *.test.mjs",
34
+ "test:journeys": "node --test test/journeys.mjs",
34
35
  "prepack": "npm run build"
35
36
  },
36
37
  "dependencies": {
37
- "@aexhq/brain": "0.18.0",
38
+ "@aexhq/brain": "0.20.0",
38
39
  "zod": "4.4.3"
39
40
  },
40
41
  "devDependencies": {}
package/src/component.mjs CHANGED
@@ -19,12 +19,20 @@ 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
- kv: JSON.parse(input.kvJson),
23
22
  events: JSON.parse(input.eventsJson),
24
23
  configuration: JSON.parse(input.configurationJson),
25
24
  system: input.system,
26
25
  tools: JSON.parse(input.toolsJson),
27
26
  }, {
27
+ setTranscript: (messages) => hosted(() => host.setTranscript(JSON.stringify(messages))),
28
+ kv: {
29
+ read: (key) => {
30
+ const value = hosted(() => host.kvRead(key));
31
+ return value === undefined ? undefined : JSON.parse(value);
32
+ },
33
+ put: (key, value) => hosted(() => host.kvPut(key, JSON.stringify(value))),
34
+ delete: (key) => hosted(() => host.kvDelete(key)),
35
+ },
28
36
  events: (after) => JSON.parse(hosted(() => host.events(BigInt(after)))),
29
37
  model: (request) => JSON.parse(hosted(() => host.model(JSON.stringify(request)))),
30
38
  dispatch: (calls) => JSON.parse(hosted(() => host.dispatch(JSON.stringify(calls)))),
@@ -32,8 +40,6 @@ export async function turn(input) {
32
40
  telemetry: (record) => host.telemetry(JSON.stringify(record ?? null)),
33
41
  });
34
42
  return {
35
- transcriptJson: JSON.stringify(output.transcript),
36
- kvJson: JSON.stringify(output.kv),
37
43
  resultJson: output.result === undefined ? undefined : JSON.stringify(output.result),
38
44
  };
39
45
  } catch (error) {
package/src/logic.mjs CHANGED
@@ -45,6 +45,8 @@ const text = (message) => message.content.filter((block) => block.type === "text
45
45
  const blockText = (block) => {
46
46
  if (block.type === "text") return block.text;
47
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]";
48
50
  const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
49
51
  return `[tool_result${block.is_error ? " (error)" : ""}] ${output.length > 2000 ? `${output.slice(0, 2000)}…` : output}`;
50
52
  };
@@ -62,8 +64,9 @@ export async function runPi(input, context) {
62
64
  };
63
65
  const placement = toolPlacement(input.tools, options);
64
66
  const transcript = cloneJson(input.transcript);
65
- const observed_sequence = await observeEvents(context, transcript, input.kv.observed_sequence ?? 0);
66
- const saved = input.kv.checkpoint;
67
+ const observed_sequence = await observeEvents(context, transcript, (await context.kv.read("observed_sequence")) ?? 0);
68
+ const saved = await context.kv.read("checkpoint");
69
+ await context.kv.put("observed_sequence", observed_sequence);
67
70
  const checkpoint = saved === undefined ? { summary: null } : cloneJson(saved);
68
71
  const body = () => checkpoint.summary === null ? transcript : transcript.slice(1);
69
72
  const shouldCompact = () =>
@@ -87,27 +90,36 @@ export async function runPi(input, context) {
87
90
  if (cut === 0) return;
88
91
  const previous = checkpoint.summary === null ? "" : `Previous summary:\n\n${checkpoint.summary}\n\n`;
89
92
  const prompt = checkpoint.summary === null ? SUMMARIZATION_PROMPT : `${UPDATE_RULES}${SUMMARIZATION_PROMPT}`;
90
- const { message } = await context.model({
93
+ const { message, stop_reason } = await context.model({
94
+ response_format: null,
91
95
  tools: [],
92
96
  messages: [{ role: "user", content: [{ type: "text", text: `${previous}${serializeConversation(messages.slice(0, cut))}\n\n${prompt}` }] }],
93
97
  });
98
+ if (stop_reason !== "end_turn") throw new Error(`Compaction did not complete: ${stop_reason}`);
94
99
  checkpoint.summary = text(message);
95
100
  transcript.splice(0, transcript.length,
96
101
  { role: "user", content: [{ type: "text", text: `${CHECKPOINT_PREFIX}${checkpoint.summary}` }] },
97
102
  ...messages.slice(cut));
98
103
  };
99
104
 
100
- 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 ?? [])] });
101
106
  for (;;) {
102
- if (shouldCompact()) await compact();
107
+ await context.setTranscript(transcript);
108
+ if (shouldCompact()) {
109
+ await compact();
110
+ await context.setTranscript(transcript);
111
+ await context.kv.put("checkpoint", checkpoint);
112
+ }
103
113
  const { message, stop_reason } = await context.model({ messages: transcript, tools: placement.definitions });
104
114
  transcript.push(message);
115
+ await context.setTranscript(transcript);
116
+ await context.kv.put("checkpoint", checkpoint);
105
117
  const calls = message.content
106
118
  .filter((block) => block.type === "tool_use")
107
119
  .map((block) => ({ call_id: block.id, name: block.name, input: block.input }));
108
120
  if (calls.length === 0) {
109
121
  await context.emit("output_emitted", { type: "assistant_message", message: text(message) });
110
- return { transcript, kv: { checkpoint, observed_sequence } };
122
+ return {};
111
123
  }
112
124
  if (stop_reason === "max_tokens") {
113
125
  transcript.push({