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 Laravel Package

packstub/agents

packstub/agents is a Laravel package that provides a clean, extensible foundation for defining and running AI agents, with support for tools, prompts, and workflows that integrate smoothly into your application.

View on GitHub
Deep Wiki
Context7

Getting Started

This release changes how the agent interprets free-text replies, so start by understanding the decision pipeline before you wire anything up.

  1. Check your current config. Look at config/agents.php for the decision classifier block and the AGENT_DECISION_CLASSIFIER_DRIVER / AGENT_DECISION_CLASSIFIER_MIN_CONFIDENCE env vars. If you were on 1.9, the default driver (agent) keeps you on the DecisionAgent side agent with identical behaviour — no action required to upgrade.
  2. Set your floor deliberately. AGENT_DECISION_CLASSIFIER_MIN_CONFIDENCE defaults to 0.8. Any decision below that becomes a question. Drivers that return no confidence (agent) are exempt, so raising the floor only affects jev and custom classifiers.
  3. First use case: take an existing tool call that ends in a proposal ("delete workspace X?") and try it with a typed reply instead of a button click — "yes go ahead", "no, not that one". That's the path the word lists can't cover and the classifier now handles.
  4. Read Decisions in words — it's the canonical reference for how lists, classifier, confidence floor and proposals interact.

Implementation Patterns

Choosing a driver

Driver When to use Confidence
agent (default) Existing apps, no external dependency, you want the side agent to reason none returned; floor ignored
jev You want ~100 ms choice questions, no reasoning text, and a real confidence signal yes
custom class You have an internal model, a hosted endpoint, or a rules layer your contract

Swapping in Jev

AGENT_DECISION_CLASSIFIER_DRIVER=jev
TYPESAFE_API_KEY=...
# optional
TYPESAFE_BASE_URL=https://...
TYPESAFE_DEFAULT_MODEL=jev-latest

Jev is per-proposal and single-question, so it scales with the number of open proposals in a turn rather than the size of the whole turn. Combined with the ~100 ms round trip, it's a reasonable choice for chat UIs where the side agent's latency is noticeable.

Writing your own classifier

Implement Packstub\Agents\Contracts\DecisionClassifier and either:

  • name the class as the driver value in config, or
  • bind it to the interface in a service provider (the more Laravel-idiomatic option, since it's container-resolved and easy to swap per-environment or in tests).

Return a confidence from your implementation — otherwise you're back to the agent-driver semantics where the floor is never enforced. Wrap the external call in a timeout and fall back to "undecided"; the framework already turns a failure into a question, so degrade rather than throw.

Tuning the floor

Treat AGENT_DECISION_CLASSIFIER_MIN_CONFIDENCE as a UX dial, not a correctness one:

  • Raise it (0.85–0.95) for destructive proposals — deletions, billing changes — where an unwanted confident-but-wrong decision is expensive.
  • Lower it (0.6–0.7) for low-stakes confirmations where re-asking is more annoying than a rare misfire.
  • Leave it alone if you're on agent; it does nothing there.

Auditing what happened

AgentTurn now records provenance, which is the cheapest observability you'll get for free:

$turn->decisionDriver();     // 'agent', 'jev', or your class name
$turn->decisionConfidence(); // lowest confidence applied; null for the side agent

Store these on your own turn/audit table, or expose them in an admin view. decisionConfidence() being the minimum across all decisions in the turn is deliberate — one shaky decision should drag the whole turn's confidence down. Log it when a turn is accepted near the floor; those are the cases most likely to be reported as "the bot did something weird".

Sync vs. queued execution

The fixed behaviour here is worth internalising, because it changes where you look when things stall:

  • A typed decision sent through AgentRun, an artisan command, or the email channel now executes in that request, not on the queue.
  • The request that completes a held decision is the one that decides — so a decision arriving via HTTP resolves synchronously if nothing else is pending.
  • But a chat click that completes a decision originally submitted through AgentRun still goes to the worker. Origin and origin of the completing action are tracked separately.

Practical implication: your tests and local debugging for AgentRun/command/email paths should assert synchronously. Don't assume Queue::fake() covers everything.

Two proposals at once

Multiple open proposals now behave correctly: a reply typed after you've decided one is read against the still-undecided set. The decision you already made stands. If the reply decides the rest, it joins the waiting turn and runs with it.

When a question arrives instead, the outstanding proposals are declined via AgentTurns::supersededResult() with the "not decided" note, and no ProposalDecided event fires for them. If your code listens on ProposalDecided to update external systems, you need a path for superseded proposals too — otherwise those rows sit in "pending" forever.

Tenancy

agents:run --tenant=acme, agents:embed --tenant=acme and the email channel now resolve slugs correctly, including through LaravelContext::findTenant() and findTenantBySlug() when no slug attribute is configured. Before this, Postgres would error on passing acme to an integer key. If you had defensive is_numeric() guards around tenant resolution, you can retire them.

Search

AgentChat::search() is now case-insensitive on Postgres via whereLike(..., caseSensitive: false). If you'd worked around the old like behaviour with a lower() index, keep the index — ilike still benefits from it — but drop any app-level normalisation of the search term.

Gotchas and Tips

  • Jev sends your data out. The typed reply and the proposal questions go to TypeSafe. If proposals embed customer names, tenant slugs, or anything sensitive, either keep the agent driver, write a self-hosted classifier, or scrub question text before it leaves.
  • The confidence floor silently does nothing on the default driver. agent returns no confidence and is explicitly exempt. If you set the env var and see no behavioural change, that's why — check decisionDriver() first.
  • The word lists still come first. The classifier is a fallback, not a replacement. Don't strip your quick-reply lists expecting the model to do better; you'll add latency and lose determinism.
  • An undecided proposal or a classifier failure also produces a question, independent of confidence. So "it asked instead of deciding" has three possible causes: below floor, no decision, or thrown exception. Log the confidence and driver together to tell them apart.
  • decisionConfidence() is null for agent — not 1.0, not 0.0. Treat null as "unknown" in any dashboard; don't average it in.
  • A custom classifier that throws will degrade to a question, not an error. That's good for uptime and bad for diagnosis. Log inside your implementation before rethrowing (or catching), because the framework will swallow the detail.
  • If you name a class as the driver, it must be resolvable by the container with no constructor args unless you've bound it first. Binding to DecisionClassifier in a provider is safer and easier to fake in tests.
  • The superseded path has no event. This is the most likely source of a stuck "pending proposal" row after upgrading. Add a listener for turn completion, or poll AgentTurns::supersededResult(), if downstream state depends on proposal resolution.
  • Queue assumptions in tests will break. Any test that pushed a decision through AgentRun and asserted a queued job now needs to assert the synchronous result instead.
  • Postgres behaviour now differs less from SQLite/MySQL — but CI is the only proof. The suite runs on all three, so run your own app's tests against Postgres before shipping, especially if you have custom LaravelContext tenant resolvers that assumed the old fallback semantics.
  • Extension point to remember: the contract is the seam. If you need per-tenant classifier selection (e.g. Jev for enterprise tenants, side agent for free tier), write a thin delegating classifier that picks a concrete implementation from LaravelContext and bind that to the interface.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource