Skip to content

Compatibility and release preparation for 1.0.0 - #8

Merged
naji247 merged 68 commits into
feat/plan-a-foundationfrom
feat/plan-f-release
Oct 22, 2026
Merged

naji247 merged 68 commits into
feat/plan-a-foundationfrom
feat/plan-f-release

Conversation

@naji247

@naji247 naji247 commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Eighth part of the PHP AgentCat SDK: compatibility and release preparation for 1.0.0. Stacked on #7; review #1 to #7 first. The PHP pages of docs.agentcat.com are in agentcathq/agentcat-ui#253.

SDK changes

  • Every tools/list page is read. When a server pages its tools (laravel/mcp pages 15 at a time), AgentCat now reads pages until it finds the called tool, so a tool on a later page keeps its own session_id or context argument instead of falling back to the heuristic strip. The rebuild stops at a repeated cursor, after 100 pages or after 5 s, and logs one line. This differs from the TypeScript SDK on purpose. Cost under PHP-FPM: a call for the last-page tool of a 300-tool laravel/mcp server takes 57.6 ms (2.7 ms before); a page-1 tool costs the same as before. The customer's own tools/list handler runs once for each page read.
  • mcp/sdk 0.9.0 is supported and tested next to 0.8.1 (a CI job without symfony/mcp-bundle, which still requires 0.8.x).
  • Hardened hosts. With functions in disable_functions (curl_multi_*, pcntl_*, posix_*, getenv, putenv, register_shutdown_function, set_time_limit, ignore_user_abort, hrtime, ini_set, usleep and more), the server keeps answering with no fatal error, and each degraded feature logs one line per process. A failure that repeats in every delivery pass logs once with a count. Without curl_multi_*, AgentCat cannot send events: the runtimes page says so.
  • track() on the Mcp\Server that a tracked Builder built returns it as already tracked, with the TypeScript SDK's log line, instead of logging a failure.
  • A customer's own get_more_tools is no longer followed by a second one from AgentCat in the same process.
  • Versions in log lines and diagnostics are bare (mcp=0.9.0, sdk=1.0.0), as in the TypeScript SDK.
  • The Symfony configuration reference has an info text for every key, and the advice for an untracked server names what to pass instead.
  • agentcat/api ~0.0.4, whose models list the agentcat:custom event type; a test fails if the SDK sends an event type that the models do not list.

Package

  • The README matches the TypeScript README: the same images (copied, linked by absolute URLs so they render on Packagist), badges, and the shared "why" and "how it works" sections word for word, except sentences that are false for PHP; each exception is listed with its reason. PHP quickstarts for plain mcp/sdk, Laravel and Symfony. A test keeps the shared sections, the images, the links and the PHP samples in step, and a nightly job compares them with the TypeScript repository.
  • CHANGELOG.md (Keep a Changelog), CONTRIBUTING.md, the Composer description and keywords of the TypeScript package, and Laravel Boost guidelines in resources/boost/guidelines/core.blade.php.
  • Comments state their rules in plain words; a test keeps internal document references out of every committed file.

CI

  • static runs both PHPStan configurations; static-lowest runs them on the lowest dependency set; PHP 8.6 runs as an experimental job.
  • The PHP 8.6 and PHP nightly jobs read phpunit-baseline.xml, which ignores one deprecation inside opis/json-schema (spl_object_hash(), deprecated in PHP 8.6). Every other deprecation still fails them, and a test reads the baseline on every PHP version.
  • package: installs the package as Packagist ships it (git archive, so export-ignore applies) into a fresh project with mcp/sdk 0.8.1 and 0.9.0, and proves that one tool call answers and delivers one event to a local collector.
  • bc: Roave's backward-compatibility check against the latest v1.* tag (it passes until 1.0.0 exists).
  • Every job has a timeout.
  • A nightly workflow: mcp/sdk, laravel/mcp and symfony/mcp-bundle at dev-main, PHP nightly, the full mutation run, composer audit, the live OpenAPI schema tests, the runtime suite on the latest image patches, the package probe on mcp/sdk dev-main, a snapshot of every member of the upstream classes AgentCat reads (a changed member it reads fails by name), and the README drift check. Each failing job opens one nightly issue, or comments on the open one, with the failing tests or members in the issue. This pull request runs it once, because it adds the workflow.

Tests

  • Traceability: every acceptance criterion of the specification has a test, or a stated reason why none is possible (3 of 413).
  • The TypeScript SDK's e2e scenarios over real HTTP on three PHP lanes: a long-lived FrankenPHP worker and two per-request php -S lanes.
  • A guard fails a test when a framework's error handler would hide a notice, warning or deprecation from src/, including during provider and bundle boot.
  • NGINX Unit with the Laravel and Symfony bridges, Symfony's mcp:server command draining on SIGTERM, a PHP-FPM pool with a shared-hosting disable_functions list, and timing tests hardened against load.

Testing

3475 tests pass (composer check; two expected skips). The covered mutation score is 98.65 % on the full set and 99 % on the lines changed since #7. The mcp/sdk 0.9.0 leg passes with 901 tests, the package probe passes for 0.8.1, 0.9.0 and dev-main, and the runtime suite passes with 62 tests in Docker. PHP 8.5 with the highest set (PHPUnit 13.4.3) and the lowest set passed locally in Docker: both PHPStan configurations and the full suite. laravel/mcp 1.1.0 (released 2026-10-20) passes.

After the merges

Tag v1.0.0 on main once #1 to #8 and agentcat-ui#253 are merged and you approve it; then the Symfony Flex recipe pull request to symfony/recipes-contrib (its text goes to you first) and the PHP lanes of the health harness.

…ollector gaps, and accept PHP 8.6's cURL option message
Laravel's HandleExceptions logs a deprecation and turns any other issue
into an exception that a request answers with an error, so a notice,
warning or deprecation that a file of src/ raises during a request never
reaches PHPUnit. After each Laravel application and Symfony kernel of the
bridge test cases boots, SrcErrorGuard puts a handler in front of the
framework's: it records such an issue with its file:line and still hands
it to the framework's handler. tearDown() puts the handler stack back and
fails the test with every recorded issue. The warm-up shares the guard's
rule for what counts as an issue of src/.
The test timed the collection of a finished transfer against 0.5 s, less
than the 1 s of slack the timing tests keep. A time cannot tell the fix
from the bug with that slack: without the fix, a finished transfer waits
one wait at most, and a wait ends after 1 s. The test now asserts an
order: after every start() and pump(), libcurl holds no finished transfer
that the transport has not read, and the fast transfer is collected while
the other one is still in transfer. That other transfer goes to a local
server that never answers, so it cannot hold a worker of the mock
collector. The loop stops before its last wait, which saves 1 s.

Reading libcurl's queue needs the transport's multi handle, so the one
helper that reads it may use reflection.
A single wait of 0.2 s with a transfer in progress returned after
0.149 s on PHP 8.6 with libcurl 7.88.1: libcurl ends a wait at a timer
of its own (one due about 200 ms after the transfer starts), and until a
curl_multi_exec() runs that timer, every later wait returns at once. The
test now counts waits: pump() and a wait of 0.2 s, for 0.6 s, run about
three times, where a wait that does not block runs about 100 000 times.
Ten waits with a NaN or a negative bound, each after a pump(), take less
than 1 s, where a wait of the maximum takes 10 s.
…the timing fixtures

- The guard goes in front of Laravel's error handler before the providers
  register (Testbench resolves RegisterProviders right after
  HandleExceptions) and in front before a Symfony kernel boots, so a
  deprecation in AgentCat's provider or bundle fails the test. A test
  provider and a compiler pass check the position; one guard fronts each
  application handler, never two.
- The guard's assertion runs in an After hook, after every tearDown() of
  the test, so its failure skips no subclass cleanup. The handler stacks
  go back in a finally, and setUp() forgets what a test that threw before
  its assertion left.
- UnreadTransfers fails closed: a member that is not what the transport
  keeps there throws a LogicException that names it.
- The positive-wait check allows fewer than 8 passes in 0.6 s, not 20.
- SrcErrorGuardTest covers a handler that declines the issue and no
  handler at all: both give the issue to PHP's own handling.
- The slow export of EventQueueDeliveryTest goes to a local server that
  never answers, so no mock collector worker serves the event after it.
…ssert the context requirement and the get_more_tools description
…runtime notes, and pin the README outline and reference links
An exit-2 reason (an unreadable snapshot, a wrong argument) went to STDERR only, so the nightly issue said nothing about it. The step now captures both streams, as the branding-drift step does.
…rt() may already have filled

Process::start() ends with one read of the pipes, into the output buffer only. When the test process lost the CPU inside start() on a loaded host, the fixture printed "signaled" before that read, the waitUntil() callback never saw it, and waitForLine() returned false when the 5 s drain ended. waitForLine() now checks the buffer, then waits for the rest. With the test process held for 0.5 s inside start(), the three tests that wait for a line failed before the fix and pass after it; 20 runs in a row pass under one busy loop per core.
…ravel Boost guidelines

CHANGELOG.md follows Keep a Changelog 1.1.0, with the 1.0.0 features under Unreleased and the B10 change with its cost. CONTRIBUTING.md is the TypeScript guide with the PHP commands and the one required check, All green; it stays out of the dist. composer.json takes the TypeScript package's description and keywords, plus php, laravel, symfony and mcp-sdk. resources/boost/guidelines/core.blade.php tells the AI agent of a Laravel application how to install, configure and test AgentCat. The package probe now requires exactly the expected top-level entries of the dist.
Every comment in src/ and tests/ now states, in plain words, the rule or the reason it relies on. Decision IDs (D07), bug IDs (B10) and criterion IDs (ac-05-E5) stay. CapturingCallTool names the tested laravel/mcp versions, 1.0.0 to 1.1.0.

Outside comments, unused test group attributes go, and three dataset names and one assertion message are reworded; no src/ code changes. PackageFilesTest guards the committed text of src/, tests/, config/, resources/, .github/, README.md, CHANGELOG.md and CONTRIBUTING.md, a reference that wraps onto the next comment line included.
…he criteria list does

The guard pattern is case-insensitive, and it now reads recipe/ and every committed root file. An explicit list allows the ordinary phrases it would otherwise flag: MCP's own tasks ("task 2 of an MCP server", "task 2 is cancelled") and another document's appendix. A data provider holds positive and negative examples. Comments spell a criterion as the criteria list does (ac-03-11).

Comments only in src/: the docblocks are reflowed, the drift reason is stated once in Drift, and CapturingToolInvoker names laravel/mcp 1.1.0. The package probe lists the dist's top-level entries with bash globs instead of GNU find's -printf.
Composer's pretty version of a tag keeps the tag's v (v0.9.0). The log
prefix, the diagnostics attributes, the events and the User-Agent now
carry the bare version (0.9.0), as the TypeScript SDK names versions, so
one release has one value across SDKs.
The changelog names no Symfony Flex recipe: the recipe reaches Flex after
the release. The tools/list line says in a customer's words that a tool on
a later page keeps its own parameters, that under per-request runtimes the
server's own tools/list handler runs once for each page read during a
tools/call, and what that costs, without IDs a reader cannot look up.

The contributing guide answers issues and pull requests as soon as we can,
with no deadline (D70).
A customer's get_more_tools on a page before the last made the last page
append AgentCat's too, so a client saw two tools of that name. A tracked
server now remembers a page that named the customer's tool and appends
nothing to its last page. A process that never read that page, every
request under PHP-FPM, cannot know it: the shadowing warning now says that
AgentCat's tool is then listed too, on the last page, and to rename the
customer's tool.
…line

With usleep() disabled, a wait for a retry threw inside the transport's
guard, and the delivery loop ran hot until its deadline: about 590,000
passes in the 5 s shutdown budget of a request whose event API failed.
A wait now sleeps with usleep(), or with time_nanosleep() where usleep()
is disabled. Where both are disabled, the wait says it cannot block, one
line per process says why, and the delivery ends at the first retry that
is not due yet.
…es only

A customer's project takes stable releases: with minimum-stability dev, a
dependency that has only dev releases would pass the probe and fail for
customers. The release legs now resolve with stable, and only the dev-main
leg keeps dev.
…ges off STDOUT

The dev-main jobs write PHPUnit's failures, STDERR included and without
colors, to a report that the issue step appends, as the snapshot and
branding jobs do, so the issue names the failing test. The PHP nightly
job prints PHP's own messages to STDERR: a dependency's deprecation can no
longer break the tests that send headers, and the log still shows it.
…d alone

The early return after the customer's own tool had become a second way to
the same answer: the record of that tool already keeps the last page free
of AgentCat's.
…eline

opis/json-schema calls spl_object_hash(), which PHP 8.6 deprecates. PHPUnit
counts the deprecation, and failOnDeprecation fails the PHP 8.6 and PHP
nightly jobs although every test passes. These two jobs now read
phpunit-baseline.xml, which lists only that line. Every other deprecation
still fails them.

The baseline stays in the repository root, because PHPUnit joins the
baseline's directory and each path without resolving "..". It is not in
phpunit.xml.dist, because Infection copies that file to a temporary
directory without rewriting the baseline path. A test reads the file, so a
broken baseline fails on every PHP version, not only in the PHP 8.6 jobs.
Base automatically changed from feat/plan-e2-symfony-runtime to feat/plan-a-foundation October 22, 2026 22:59
@naji247
naji247 merged commit 91e338c into feat/plan-a-foundation Oct 22, 2026
29 checks passed
@naji247
naji247 deleted the feat/plan-f-release branch October 22, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants