@oh-my-pi/pi-agent-core 15.11.1 → 15.11.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.11.2] - 2026-06-11
6
+
7
+ ### Added
8
+
9
+ - `AgentTool.concurrency` now also accepts a per-call resolver function `(args) => "shared" | "exclusive"`, letting tools pick the scheduling mode from the call's arguments (a throwing resolver falls back to `"exclusive"`)
10
+
11
+ ### Fixed
12
+
13
+ - Fixed whitespace-only error tool results so Anthropic requests no longer 400 with `tool_result: content cannot be empty if is_error is true` and wedge the session on every subsequent turn
5
14
  ## [15.11.0] - 2026-06-10
6
15
  ### Breaking Changes
7
16
 
@@ -382,8 +382,9 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
382
382
  * Concurrency mode for tool scheduling when multiple calls are in one turn.
383
383
  * - "shared": can run alongside other shared tools (default)
384
384
  * - "exclusive": runs alone; other tools wait until it finishes
385
+ * - function: resolved per call from the (raw, pre-validation) arguments
385
386
  */
386
- concurrency?: "shared" | "exclusive";
387
+ concurrency?: "shared" | "exclusive" | ((args: Partial<Static<TParameters>>) => "shared" | "exclusive");
387
388
  /** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
388
389
  lenientArgValidation?: boolean;
389
390
  /**
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": "15.11.1",
4
+ "version": "15.11.3",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -35,11 +35,11 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "15.11.1",
39
- "@oh-my-pi/pi-catalog": "15.11.1",
40
- "@oh-my-pi/pi-natives": "15.11.1",
41
- "@oh-my-pi/pi-utils": "15.11.1",
42
- "@oh-my-pi/snapcompact": "15.11.1",
38
+ "@oh-my-pi/pi-ai": "15.11.3",
39
+ "@oh-my-pi/pi-catalog": "15.11.3",
40
+ "@oh-my-pi/pi-natives": "15.11.3",
41
+ "@oh-my-pi/pi-utils": "15.11.3",
42
+ "@oh-my-pi/snapcompact": "15.11.3",
43
43
  "@opentelemetry/api": "^1.9.1"
44
44
  },
45
45
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -148,6 +148,16 @@ function snapshotAssistantMessageEvent(event: AssistantMessageEvent): AssistantM
148
148
  * (missing `content` array → crash on reload). We coerce at the single boundary where untyped
149
149
  * results enter the agent loop, so every downstream consumer can rely on the type.
150
150
  */
151
+ const EMPTY_ERROR_TOOL_RESULT_TEXT = "Tool failed with no output.";
152
+
153
+ function hasSubstantiveToolResultContent(content: AgentToolResult["content"]): boolean {
154
+ for (const block of content) {
155
+ if (block.type === "image") return true;
156
+ if (block.type === "text" && block.text.trim().length > 0) return true;
157
+ }
158
+ return false;
159
+ }
160
+
151
161
  function coerceToolResult(raw: unknown): { result: AgentToolResult<unknown>; malformed: boolean } {
152
162
  const rawObj = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : null;
153
163
  const rawContent = rawObj?.content;
@@ -193,8 +203,14 @@ function coerceToolResult(raw: unknown): { result: AgentToolResult<unknown>; mal
193
203
  text: `Tool returned an invalid result: ${invalidBlocks} content block${invalidBlocks === 1 ? "" : "s"} had an unsupported shape.`,
194
204
  });
195
205
  }
206
+ const isError = explicitError || invalidBlocks > 0;
207
+ // Anthropic rejects tool_result blocks with is_error: true and empty content.
208
+ if (isError && !hasSubstantiveToolResultContent(content)) {
209
+ content.length = 0;
210
+ content.push({ type: "text", text: EMPTY_ERROR_TOOL_RESULT_TEXT });
211
+ }
196
212
  return {
197
- result: { content, details, ...(explicitError || invalidBlocks > 0 ? { isError: true } : {}) },
213
+ result: { content, details, ...(isError ? { isError: true } : {}) },
198
214
  malformed: invalidBlocks > 0,
199
215
  };
200
216
  }
@@ -1547,7 +1563,19 @@ async function executeToolCalls(
1547
1563
 
1548
1564
  for (let index = 0; index < records.length; index++) {
1549
1565
  const record = records[index];
1550
- const concurrency = record.tool?.concurrency ?? "shared";
1566
+ const concurrencyMode = record.tool?.concurrency;
1567
+ let concurrency: "shared" | "exclusive";
1568
+ if (typeof concurrencyMode === "function") {
1569
+ // Resolved from raw pre-validation args; a throwing resolver must not
1570
+ // take down the whole batch, so fall back to the safe (serial) mode.
1571
+ try {
1572
+ concurrency = concurrencyMode(record.args);
1573
+ } catch {
1574
+ concurrency = "exclusive";
1575
+ }
1576
+ } else {
1577
+ concurrency = concurrencyMode ?? "shared";
1578
+ }
1551
1579
  const start = concurrency === "exclusive" ? Promise.all([lastExclusive, ...sharedTasks]) : lastExclusive;
1552
1580
  const task = start.then(() => runTool(record, index));
1553
1581
  tasks.push(task);
package/src/types.ts CHANGED
@@ -456,8 +456,9 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
456
456
  * Concurrency mode for tool scheduling when multiple calls are in one turn.
457
457
  * - "shared": can run alongside other shared tools (default)
458
458
  * - "exclusive": runs alone; other tools wait until it finishes
459
+ * - function: resolved per call from the (raw, pre-validation) arguments
459
460
  */
460
- concurrency?: "shared" | "exclusive";
461
+ concurrency?: "shared" | "exclusive" | ((args: Partial<Static<TParameters>>) => "shared" | "exclusive");
461
462
  /** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
462
463
  lenientArgValidation?: boolean;
463
464
  /**