Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
particle-academy/prism-harness
claude-code
cursor
agent-sessions
threads
tool-permissions
subagents
fancy-flow
mirrored-contracts
optional-dependency

Durable agent sessions for Laravel: threads, modes, tool permissions, subagents.

Install
php artisan boost:add-skill particle-academy/prism-harness

Save this content to: AGENTS.md

# AGENTS.md — particle-academy/prism-harness

Durable agent sessions for Laravel: threads, modes, tool permissions, subagents.

> **Read [the shared guide](https://github.com/Particle-Academy/prism-parity/blob/main/docs/AGENTS.md)
> first** — the boundary, the gates, the binding decisions, the review skills.
> This file is only what is true of *this* repository.

## What this package owns, and what it must not absorb

It owns **the conversation**: a thread that survives the request, the mode a
session is running in, which tools that session may reach, and subagents.

It does **not** own retrieval — `prism-memory` reads threads and stores derived
representations, and the two compose through Prism's own message value objects
rather than depending on each other. Keep it that way: a hard dependency in
either direction means you cannot have sessions without memory or memory without
sessions, and both are reasonable things to want.

## The fancy-flow bridge is the sharpest edge in the repo

`src/Flow/` bridges this package to `particle-academy/fancy-flow-php`, and the
whole arrangement is unusual enough to be worth understanding before touching.

**It is `suggest`, never `require`.** fancy-flow needs PHP 8.4; this package
supports 8.2. A hard dependency would drop two supported versions to gain an
integration most consumers do not use. Nothing autoloads `Flow/` unless an
application wires it.

**Which means the contracts are MIRRORED, and the mirror is the risk.**
Because the real package cannot be installed across the matrix, the interfaces
are declared locally when absent:

- `tests/Pest.php` — `eval`'d interface declarations, so the tests run everywhere
- `stubs/fancy-flow.php` — the same shapes, for static analysis
- `phpstan.neon.dist` → `scanFiles` must list the stub
- `composer-require-checker.json` → the whitelist takes **exact symbol names**, not prefixes

**Nothing in CI notices if upstream changes.** That is inherent, not an
oversight: there is no version of this that catches a rename in a package we
cannot install. So the mirror is verified by hand against the real package —
installed temporarily — and the verification is re-run whenever fancy-flow
moves. `tests/Pest.php` records which version was checked. Update that note when
you re-check; a stale version number there is worse than none.

There is one test whose entire job is to fail loudly if either side is renamed
locally (`it('implements the contract fancy-flow actually declares')`). It
cannot catch an upstream rename. Do not mistake it for coverage that it is not.

**Currently behind: the mirror was verified against v0.41.0, and v0.42.0 has
shipped.** The change is additive — `tool_calls` moved from undeclared-but-read
to declared on the contract, which is why nothing here broke — but the mirror
and the note in `tests/Pest.php` are owed a re-verification against v0.42.0.
This is exactly the drift the arrangement is known to have; it is written down
here rather than left for the next person to discover.

## Two things about the seam that read as bugs and are not

**`withMaxSteps(1)` when tools are present.** fancy-flow's `AgentExecutor` owns
the loop — it invokes tools itself and calls back. Letting Prism run its own
loop as well would execute every tool twice and hide half the trace from the
workflow's audit.

**`tool_calls` is emitted on the result array.** As of fancy-flow-php v0.42.0
this is declared on their contract; before that it was undeclared and read
anyway, because the agent node's tool loop is dead without it. Both key
spellings their executor accepts are covered.

## Prism's ToolError is why `HarnessToolInvoker` looks defensive

`Prism\Prism\Tool::handle()` **catches** a handler exception and **returns** a
`ToolError` value rather than propagating. An invoker that only caught
exceptions would hand that object to the workflow as a successful result, where
a downstream node reads a failure as data.

Hence the `ToolError` branch, and hence a tool that is not on the allowlist
**throws** rather than returning "unknown tool" as a result: a model handed that
as a result treats it as information and burns the step budget guessing at an
allowlist it cannot see, while the workflow author who typed the wrong name sees
only a mediocre answer.

## The shipped-configuration trap, which this package taught the ecosystem

This package once shipped a Redis default that broke every fresh install on a
machine without Redis, and the suite was green throughout — because every test
set its stores explicitly, so not one of them exercised what an installing
application actually receives.

That became decision
[0012](https://github.com/Particle-Academy/prism-parity/blob/main/docs/decisions/0012-test-the-shipped-configuration.md).
Anything you add to `config/` needs a test that reads the **shipped** file.

## Gates

```sh
composer test && composer types && composer format
```

CI runs `tests`, `phpstan`, `formatting` and `require-checker` separately. Run
all of them.

## Cutting a release, and the step that is easy to skip

```sh
git push origin main
# WAIT for the tests workflow to go GREEN on the commit you are about to tag
gh run watch "$(gh run list --workflow=tests.yml --limit 1 --json databaseId --jq '.[0].databaseId')"

git tag -a vX.Y.Z -m "…"     # ANNOTATED. The message IS the release notes.
git push origin vX.Y.Z

# THEN CHECK IT ACTUALLY PUBLISHED — this is the step that gets skipped
gh run list --workflow=publish.yml --limit 1
gh release view vX.Y.Z
```

**The release workflow refuses to publish a tag whose tests have not already
succeeded for that exact commit.** That guard is right, and it means pushing
main and the tag in the same breath FAILS — the tests for that SHA are still
queued when the tag lands.

That is not hypothetical. v0.6.0, v0.6.1 and v0.7.0 were all tagged that way in
one afternoon; all three release runs failed on the guard, and nobody noticed
because pushing the tag looks like success at the terminal. The releases page
went on showing v0.5.0 as Latest while three newer versions existed as tags, and
a downstream consumer had to diff the source tree to find out what changed.

**A pushed tag is not a release.** `git push` succeeding tells you the tag
exists, not that anything published notes for it. Check `gh release view`, every
time. Composer resolves from tags, so the package installs either way — which is
exactly why this failure is silent.

Two other things the guard enforces, worth knowing before you are surprised by
them:

- **A lightweight tag is refused.** The annotation is the release notes; `git tag`
  without `-a` carries none, and the workflow will not invent them.
- **Any other gate failing on that commit blocks the release**, including the
  NIGHTLY strict currency check in `factcheck`. That one can redden a commit
  hours after you tagged it, because it fails on claim staleness across the
  ecosystem rather than on anything in this repo — so a tag that released fine
  at the time can become unreleasable later. Reconciling `factcheck.lock.json`
  in `prism-parity` is the fix; blanket `--reconcile` is not, because it
  re-records the claim census and will quietly bless a rule that has stopped
  matching anything.

AGENTS.md — particle-academy/prism-harness

Durable agent sessions for Laravel: threads, modes, tool permissions, subagents.

Read the shared guide first — the boundary, the gates, the binding decisions, the review skills. This file is only what is true of this repository.

What this package owns, and what it must not absorb

It owns the conversation: a thread that survives the request, the mode a session is running in, which tools that session may reach, and subagents.

It does not own retrieval — prism-memory reads threads and stores derived representations, and the two compose through Prism's own message value objects rather than depending on each other. Keep it that way: a hard dependency in either direction means you cannot have sessions without memory or memory without sessions, and both are reasonable things to want.

The fancy-flow bridge is the sharpest edge in the repo

src/Flow/ bridges this package to particle-academy/fancy-flow-php, and the whole arrangement is unusual enough to be worth understanding before touching.

It is suggest, never require. fancy-flow needs PHP 8.4; this package supports 8.2. A hard dependency would drop two supported versions to gain an integration most consumers do not use. Nothing autoloads Flow/ unless an application wires it.

Which means the contracts are MIRRORED, and the mirror is the risk. Because the real package cannot be installed across the matrix, the interfaces are declared locally when absent:

  • tests/Pest.php — eval'd interface declarations, so the tests run everywhere
  • stubs/fancy-flow.php — the same shapes, for static analysis
  • phpstan.neon.dist → scanFiles must list the stub
  • composer-require-checker.json → the whitelist takes exact symbol names, not prefixes

Nothing in CI notices if upstream changes. That is inherent, not an oversight: there is no version of this that catches a rename in a package we cannot install. So the mirror is verified by hand against the real package — installed temporarily — and the verification is re-run whenever fancy-flow moves. tests/Pest.php records which version was checked. Update that note when you re-check; a stale version number there is worse than none.

There is one test whose entire job is to fail loudly if either side is renamed locally (it('implements the contract fancy-flow actually declares')). It cannot catch an upstream rename. Do not mistake it for coverage that it is not.

Currently behind: the mirror was verified against v0.41.0, and v0.42.0 has shipped. The change is additive — tool_calls moved from undeclared-but-read to declared on the contract, which is why nothing here broke — but the mirror and the note in tests/Pest.php are owed a re-verification against v0.42.0. This is exactly the drift the arrangement is known to have; it is written down here rather than left for the next person to discover.

Two things about the seam that read as bugs and are not

withMaxSteps(1) when tools are present. fancy-flow's AgentExecutor owns the loop — it invokes tools itself and calls back. Letting Prism run its own loop as well would execute every tool twice and hide half the trace from the workflow's audit.

tool_calls is emitted on the result array. As of fancy-flow-php v0.42.0 this is declared on their contract; before that it was undeclared and read anyway, because the agent node's tool loop is dead without it. Both key spellings their executor accepts are covered.

Prism's ToolError is why HarnessToolInvoker looks defensive

Prism\Prism\Tool::handle() catches a handler exception and returns a ToolError value rather than propagating. An invoker that only caught exceptions would hand that object to the workflow as a successful result, where a downstream node reads a failure as data.

Hence the ToolError branch, and hence a tool that is not on the allowlist throws rather than returning "unknown tool" as a result: a model handed that as a result treats it as information and burns the step budget guessing at an allowlist it cannot see, while the workflow author who typed the wrong name sees only a mediocre answer.

The shipped-configuration trap, which this package taught the ecosystem

This package once shipped a Redis default that broke every fresh install on a machine without Redis, and the suite was green throughout — because every test set its stores explicitly, so not one of them exercised what an installing application actually receives.

That became decision 0012. Anything you add to config/ needs a test that reads the shipped file.

Gates

composer test && composer types && composer format

CI runs tests, phpstan, formatting and require-checker separately. Run all of them.

Cutting a release, and the step that is easy to skip

git push origin main
# WAIT for the tests workflow to go GREEN on the commit you are about to tag
gh run watch "$(gh run list --workflow=tests.yml --limit 1 --json databaseId --jq '.[0].databaseId')"

git tag -a vX.Y.Z -m "…"     # ANNOTATED. The message IS the release notes.
git push origin vX.Y.Z

# THEN CHECK IT ACTUALLY PUBLISHED — this is the step that gets skipped
gh run list --workflow=publish.yml --limit 1
gh release view vX.Y.Z

The release workflow refuses to publish a tag whose tests have not already succeeded for that exact commit. That guard is right, and it means pushing main and the tag in the same breath FAILS — the tests for that SHA are still queued when the tag lands.

That is not hypothetical. v0.6.0, v0.6.1 and v0.7.0 were all tagged that way in one afternoon; all three release runs failed on the guard, and nobody noticed because pushing the tag looks like success at the terminal. The releases page went on showing v0.5.0 as Latest while three newer versions existed as tags, and a downstream consumer had to diff the source tree to find out what changed.

A pushed tag is not a release. git push succeeding tells you the tag exists, not that anything published notes for it. Check gh release view, every time. Composer resolves from tags, so the package installs either way — which is exactly why this failure is silent.

Two other things the guard enforces, worth knowing before you are surprised by them:

  • A lightweight tag is refused. The annotation is the release notes; git tag without -a carries none, and the workflow will not invent them.
  • Any other gate failing on that commit blocks the release, including the NIGHTLY strict currency check in factcheck. That one can redden a commit hours after you tagged it, because it fails on claim staleness across the ecosystem rather than on anything in this repo — so a tag that released fine at the time can become unreleasable later. Reconciling factcheck.lock.json in prism-parity is the fix; blanket --reconcile is not, because it re-records the claim census and will quietly bless a rule that has stopped matching anything.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk