Skip to content

v0.13.0 — Adopt laravel/ai 0.8 + dependency hygiene

Choose a tag to compare

@dberry37388 dberry37388 released this 23 Jun 19:58
ce2c3d1

Adopt laravel/ai 0.8 (floor raise from ^0.6 || ^0.7) + dependency hygiene — surfaces MCP client/server tool support and OpenAI ZDR encrypted reasoning, hardens the streaming-event mapping against silent drops, and refreshes dev-tooling floors and the supported matrix.

Added

  • Surface laravel/ai 0.8 MCP tool support through Swarm's passthrough, with a degrade-safe tool-result boundary (#256). laravel/ai 0.8 adds MCP client/server tool support. Swarm's tool model is pure passthrough — a ToolCall/ToolResult is carried as an opaque object through tool-call capture, the SnapshotToolCallNormalizer, the step memory snapshot, and the streamed SwarmToolCall/SwarmToolResult events — so MCP-backed tools flow through with no MCP-specific code, including a structured (non-scalar) MCP result, which is preserved intact end to end (verified by a new tests/Feature/Tools/McpToolPassthroughTest.php that round-trips a structured MCP result through capture, the snapshot's encodeJsondecodeJson, and persisted stream replay). A tool result is typed mixed, so at the edges it can be a value JSON cannot represent (a binary-ish MCP result with invalid UTF-8, recursion, a resource-like value); previously that would throw a JsonException up through the strict shared JSON encoder and crash a live run. A new BuiltByBerry\LaravelSwarm\Support\ToolResultEncoding helper now degrades at the tool I/O boundaries only — the snapshot recorder's tool_calls column (DatabaseMemorySnapshotRecorder) and the streamed swarm_tool_call / swarm_tool_result events (SwarmToolCall::toArray() / SwarmToolResult::toArray(), whose payload the strict DatabaseStreamEventStore then encodes unchanged): an unencodable result is replaced by a fixed, pure-scalar typed placeholder ({"__swarm_unencodable_tool_result__": true, "tool": "<name>"}) that never echoes the original payload, and an unencodable set of call arguments (the same mixed shape, sharing the identical strict-encode crash path) degrades the same way to {"__swarm_unencodable_tool_arguments__": true, "tool": "<name>"}, each with a class-only breadcrumb (the tool name + exception class, never the payload bytes — mirroring the #257 and v0.12.3 SafeReporting discipline, routed through never-throw safeLog()/error_log). The shared strict InteractsWithJsonColumns::encodeJson (JSON_THROW_ON_ERROR) the nine audit/durable/resume/evidence stores depend on is left strict and untouched — the degrade is site-scoped to tool I/O, where the placeholder is valid mixed and resume is unaffected (the replay coordinator does not read tool-call results). The strict decodeJson read path also stays strict: a decode throw still means genuine corruption/tamper. The snapshot payload column and every non-tool stream event keep failing loud. Operator note: an …a tool result could not be JSON-encoded for persistence; substituting a typed placeholder… breadcrumb means a single tool's output was not JSON-representable and was recorded as the placeholder; the run itself completed normally.

  • Adopt laravel/ai ^0.8 with regression guards against the upgrade's silent-drift surfaces (#255). Three guards land alongside the floor raise, each targeting a 0.8 contract change a green suite on the old floor would otherwise miss: an event-set lock-step test (tests/Unit/Streaming/LaravelAiStreamEventSetTest.php) enumerates the installed Laravel\Ai\Streaming\Events\* classes and fails if the set drifts from the runners' triaged handled/ignored split — so a future bump that adds a content-bearing event can no longer slip silently into the log-once breadcrumb (the else added in #257) and vanish from the durable snapshot; a snapshot backward-compatibility fixture (tests/Fixtures/Persistence/pre_0_8_tool_calls.json + tests/Unit/Memory/SnapshotToolCallBackwardCompatTest.php) — a literal pre-upgrade tool_calls blob, not one regenerated by the current normalizer — asserts it rehydrates byte-identically under 0.8 and that SnapshotToolCallNormalizer stays pinned to its five keys (id, name, arguments, result, result_id) regardless of the reasoning fields 0.8 added to Data\ToolCall; and a ZDR end-to-end stream test (tests/Feature/ZdrReasoningStreamTest.php) drives a reasoning stream with null summaries and an opaque reasoningEncryptedContent tool call through both runner capture paths, proving the run completes without crashing and the encrypted blob never reaches swarm's event contract.

Changed

  • Raise the minimum laravel/ai to ^0.8 (drop ^0.6 || ^0.7) (#255). laravel/ai shipped v0.8.1 outside the prior constraint; it is Swarm's core runtime spine, and the 0.8 dependency adds MCP tool support (surfacing it through Swarm is tracked separately in #256) and OpenAI ZDR (zero-data-retention) encrypted reasoning. From an operator's view nothing changes; from a consumer's view the minimum supported laravel/ai rises to 0.8. The one consumer-visible shape change is defensive: SwarmToolCall::toArray() previously delegated to Laravel\Ai\Responses\Data\ToolCall::toArray(), which in 0.8 began emitting a reasoning_encrypted_content key (the opaque ZDR reasoning blob). The streamed/persisted swarm_tool_call event is now pinned to the exact field set it already round-trips through fromArray()id, name, arguments, result_id, reasoning_id, reasoning_summary — so the swarm event contract stays swarm-owned and insulated from upstream DTO drift, and the encrypted reasoning blob (which Swarm never echoes back to the provider) is deliberately excluded from the broadcast/persistence stream. The durable snapshot's tool_calls shape is unchanged, so pre-upgrade snapshots replay byte-identically. Breaking: consumers pinned to laravel/ai below 0.8 must upgrade.

  • Refuse known-vulnerable guzzlehttp/guzzle (< 7.12.1) via a conflict constraint. The composer audit CI job (added in v0.12.2, non-blocking) flagged two medium advisories — CVE-2026-55767 (dot-only cookie domains match all hosts) and CVE-2026-55568 (silent HTTPS proxy downgrade to cleartext) — both fixed in guzzle 7.12.1. Guzzle is a purely transitive dependency here (pulled by laravel/framework and aws/aws-sdk-php), and only the --prefer-lowest resolution picked the vulnerable range; --prefer-stable already resolved a patched version. A root conflict on guzzlehttp/guzzle: <7.12.1 floors the transitive resolution above the advisory without taking a direct runtime dependency on a package Swarm does not use, so the lowest-resolution audit is advisory-clean. No require constraint changed; the full suite passes under --prefer-lowest with guzzle 7.12.1.

  • Refresh dev-tooling dependency floors so the --prefer-lowest CI lane tests a current minimum (#258). The require-dev floors for orchestra/testbench (^11.0^11.1), pestphp/pest (^4.4^4.7), larastan/larastan (^3.0^3.10), and laravel/pint (^1.0^1.29) are raised so the lowest in-range resolution pulls in upstream fixes instead of long-stale tooling. Dev-only — no require (runtime) constraint changed, so this is invisible to consumers. The full suite (composer test, composer analyse, vendor/bin/pint --test) passes under --prefer-lowest --prefer-stable on the raised floors (resolved: testbench v11.1.0, pest v4.7.0, larastan v3.10.0, pint v1.29.0).

Documentation

  • Refresh the supported matrix for the laravel/ai 0.8 floor raise (#259). The README and Getting Started requirements/prerequisites now state the matrix to match the package's actual constraints — PHP ^8.5 (a deliberate floor built on 8.5 language features), Laravel 13 (illuminate/* ^13.0), and laravel/ai ^0.8 — replacing the stale laravel/ai ^0.6 line. Both pages note, where consumers look, that 0.6 / 0.7 support was dropped in v0.13.0 and that applications pinned below laravel/ai 0.8 must upgrade. Docs only — no code or constraint change (the floor raise itself is #255, above).

Fixed

  • Unrecognized AI stream events now leave a fail-visible breadcrumb instead of vanishing from the durable snapshot (#257). Both streaming runners map the provider event stream with an instanceof if/elseif chain (SequentialRunner::stream() and StaticHierarchicalStreamRunner::streamAgentEvents()). Neither chain had a final else, so an event type that matched no branch — most plausibly a new event surfaced by an upstream laravel/ai bump — was dropped silently: it never mapped to a SwarmStreamEvent and never reached the frozen snapshot, leaving the durable replay source quietly incomplete with no signal. Both chains now end in a degrade-safe else that records the unrecognized event class (never its payload) into a per-step set and emits one warning per step from the existing finally — mirroring the per-run aggregation of the checkpoint-failure warning, so a high-frequency new event type logs once rather than flooding. The breadcrumb logs, never throws: a harmless new provider event cannot abort an otherwise-successful run. Only the class name is logged — an unrecognized event may carry un-redacted user/agent content and the breadcrumb is emitted outside the SwarmCapture redaction pipeline, so it holds the same class-only discipline as SafeReporting — and the emit is routed through SafeReporting's never-throw safeLog(), so a hostile or misconfigured logger cannot turn the breadcrumb into a second failure surface that masks an in-flight stream exception (the recorded context is run_id, step_index, and the list of dropped event types). Operator note: this makes the drop visible, not recovered — a laravel-swarm: <n> unrecognized stream event type(s) were dropped from the snapshot … warning means content for that step is missing from the durable record, and (because replay reads the frozen snapshot, not the live provider) the warning fires only on the original live run, not on replay; the log line is the sole evidence. Whether a newly surfaced event type should be mapped into the snapshot is a separate decision handled as part of the laravel/ai 0.8 adoption. To carry the breadcrumb, SequentialStreamRunner (the static runner's parent) now receives a Psr\Log\LoggerInterface via constructor injection — autowired, no binding or configuration change. Covered by a new behavioral chain-parity suite: every handled event type breadcrumbs in neither runner, an unrecognized event breadcrumbs in both, the run still completes, and the logged record is asserted to contain the event class but not its payload.