v0.13.0 — Adopt laravel/ai 0.8 + dependency hygiene
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/ai0.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 — aToolCall/ToolResultis carried as an opaque object through tool-call capture, theSnapshotToolCallNormalizer, the step memory snapshot, and the streamedSwarmToolCall/SwarmToolResultevents — 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 newtests/Feature/Tools/McpToolPassthroughTest.phpthat round-trips a structured MCP result through capture, the snapshot'sencodeJson→decodeJson, and persisted stream replay). A tool result is typedmixed, 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 aJsonExceptionup through the strict shared JSON encoder and crash a live run. A newBuiltByBerry\LaravelSwarm\Support\ToolResultEncodinghelper now degrades at the tool I/O boundaries only — the snapshot recorder'stool_callscolumn (DatabaseMemorySnapshotRecorder) and the streamedswarm_tool_call/swarm_tool_resultevents (SwarmToolCall::toArray()/SwarmToolResult::toArray(), whose payload the strictDatabaseStreamEventStorethen 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 samemixedshape, 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.3SafeReportingdiscipline, routed through never-throwsafeLog()/error_log). The shared strictInteractsWithJsonColumns::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 validmixedand resume is unaffected (the replay coordinator does not read tool-call results). The strictdecodeJsonread path also stays strict: a decode throw still means genuine corruption/tamper. The snapshotpayloadcolumn 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.8with 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 installedLaravel\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 (theelseadded 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-upgradetool_callsblob, not one regenerated by the current normalizer — asserts it rehydrates byte-identically under 0.8 and thatSnapshotToolCallNormalizerstays pinned to its five keys (id,name,arguments,result,result_id) regardless of the reasoning fields 0.8 added toData\ToolCall; and a ZDR end-to-end stream test (tests/Feature/ZdrReasoningStreamTest.php) drives a reasoning stream with null summaries and an opaquereasoningEncryptedContenttool 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/aito^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 supportedlaravel/airises to 0.8. The one consumer-visible shape change is defensive:SwarmToolCall::toArray()previously delegated toLaravel\Ai\Responses\Data\ToolCall::toArray(), which in 0.8 began emitting areasoning_encrypted_contentkey (the opaque ZDR reasoning blob). The streamed/persistedswarm_tool_callevent is now pinned to the exact field set it already round-trips throughfromArray()—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'stool_callsshape is unchanged, so pre-upgrade snapshots replay byte-identically. Breaking: consumers pinned tolaravel/aibelow 0.8 must upgrade. -
Refuse known-vulnerable
guzzlehttp/guzzle(< 7.12.1) via aconflictconstraint. Thecomposer auditCI 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 guzzle7.12.1. Guzzle is a purely transitive dependency here (pulled bylaravel/frameworkandaws/aws-sdk-php), and only the--prefer-lowestresolution picked the vulnerable range;--prefer-stablealready resolved a patched version. A rootconflictonguzzlehttp/guzzle: <7.12.1floors 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. Norequireconstraint changed; the full suite passes under--prefer-lowestwith guzzle7.12.1. -
Refresh dev-tooling dependency floors so the
--prefer-lowestCI lane tests a current minimum (#258). Therequire-devfloors fororchestra/testbench(^11.0→^11.1),pestphp/pest(^4.4→^4.7),larastan/larastan(^3.0→^3.10), andlaravel/pint(^1.0→^1.29) are raised so the lowest in-range resolution pulls in upstream fixes instead of long-stale tooling. Dev-only — norequire(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-stableon the raised floors (resolved: testbenchv11.1.0, pestv4.7.0, larastanv3.10.0, pintv1.29.0).
Documentation
- Refresh the supported matrix for the
laravel/ai0.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), andlaravel/ai^0.8 — replacing the stalelaravel/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 belowlaravel/ai0.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
instanceofif/elseif chain (SequentialRunner::stream()andStaticHierarchicalStreamRunner::streamAgentEvents()). Neither chain had a finalelse, so an event type that matched no branch — most plausibly a new event surfaced by an upstreamlaravel/aibump — was dropped silently: it never mapped to aSwarmStreamEventand never reached the frozen snapshot, leaving the durable replay source quietly incomplete with no signal. Both chains now end in a degrade-safeelsethat records the unrecognized event class (never its payload) into a per-step set and emits onewarningper step from the existingfinally— 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 theSwarmCaptureredaction pipeline, so it holds the same class-only discipline asSafeReporting— and the emit is routed throughSafeReporting's never-throwsafeLog(), 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 isrun_id,step_index, and the list of dropped event types). Operator note: this makes the drop visible, not recovered — alaravel-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 thelaravel/ai0.8 adoption. To carry the breadcrumb,SequentialStreamRunner(the static runner's parent) now receives aPsr\Log\LoggerInterfacevia 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.