@oh-my-pi/pi-agent-core 14.7.3 → 14.7.4

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.
Files changed (2) hide show
  1. package/package.json +4 -4
  2. package/src/agent-loop.ts +45 -7
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-agent-core",
4
- "version": "14.7.3",
4
+ "version": "14.7.4",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -35,9 +35,9 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "14.7.3",
39
- "@oh-my-pi/pi-natives": "14.7.3",
40
- "@oh-my-pi/pi-utils": "14.7.3"
38
+ "@oh-my-pi/pi-ai": "14.7.4",
39
+ "@oh-my-pi/pi-natives": "14.7.4",
40
+ "@oh-my-pi/pi-utils": "14.7.4"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@sinclair/typebox": "^0.34.49",
package/src/agent-loop.ts CHANGED
@@ -25,6 +25,46 @@ import type {
25
25
  /** Sentinel returned by the abort race in `streamAssistantResponse`. */
26
26
  const ABORTED: unique symbol = Symbol("agent-loop-aborted");
27
27
 
28
+ /**
29
+ * Normalize a value coming back from `tool.execute()` (or its streaming partial-update callback)
30
+ * into a structurally valid {@link AgentToolResult}.
31
+ *
32
+ * The tool interface is typed, but third-party tools (MCP, extensions, user-authored AgentTools)
33
+ * can violate the contract at runtime. Persisting a malformed result corrupts the session file
34
+ * (missing `content` array → crash on reload). We coerce at the single boundary where untyped
35
+ * results enter the agent loop, so every downstream consumer can rely on the type.
36
+ */
37
+ function coerceToolResult(raw: unknown): { result: AgentToolResult<any>; malformed: boolean } {
38
+ const rawObj = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : null;
39
+ const rawContent = rawObj?.content;
40
+ const details = rawObj && "details" in rawObj ? rawObj.details : {};
41
+
42
+ if (!Array.isArray(rawContent)) {
43
+ return {
44
+ result: {
45
+ content: [{ type: "text", text: "Tool returned an invalid result: missing content array." }],
46
+ details,
47
+ },
48
+ malformed: true,
49
+ };
50
+ }
51
+
52
+ const content: AgentToolResult["content"] = [];
53
+ for (const block of rawContent) {
54
+ if (!block || typeof block !== "object" || !("type" in block)) continue;
55
+ if (block.type === "text" && typeof (block as { text?: unknown }).text === "string") {
56
+ content.push({ type: "text", text: sanitizeText((block as { text: string }).text) });
57
+ } else if (
58
+ block.type === "image" &&
59
+ typeof (block as { data?: unknown }).data === "string" &&
60
+ typeof (block as { mimeType?: unknown }).mimeType === "string"
61
+ ) {
62
+ content.push(block as { type: "image"; data: string; mimeType: string });
63
+ }
64
+ }
65
+ return { result: { content, details }, malformed: false };
66
+ }
67
+
28
68
  /**
29
69
  * Start an agent loop with a new prompt message.
30
70
  * The prompt is added to the context and events are emitted for it.
@@ -656,7 +696,7 @@ async function executeToolCalls(
656
696
  toolCalls: toolCallInfos,
657
697
  })
658
698
  : undefined;
659
- result = await tool.execute(
699
+ const rawResult = await tool.execute(
660
700
  toolCall.id,
661
701
  transformToolCallArguments ? transformToolCallArguments(effectiveArgs, toolCall.name) : effectiveArgs,
662
702
  tool.nonAbortable ? undefined : toolSignal,
@@ -666,16 +706,14 @@ async function executeToolCalls(
666
706
  toolCallId: toolCall.id,
667
707
  toolName: toolCall.name,
668
708
  args: argsForExecution,
669
- partialResult: {
670
- ...partialResult,
671
- content: partialResult.content.map(c =>
672
- c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c,
673
- ),
674
- },
709
+ partialResult: coerceToolResult(partialResult).result,
675
710
  });
676
711
  },
677
712
  toolContext,
678
713
  );
714
+ const coerced = coerceToolResult(rawResult);
715
+ result = coerced.result;
716
+ if (coerced.malformed) isError = true;
679
717
  } catch (e) {
680
718
  result = {
681
719
  content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],