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

Laravel Copilot Sdk Laravel Package

revolution/laravel-copilot-sdk

A Laravel SDK that wraps Copilot's API in an expressive, configurable package. Quickly authenticate, send requests, and handle responses using Laravel-friendly classes, facades, and configuration—ideal for adding Copilot-powered workflows to your PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites are the real onboarding step. This package is a thin JSON-RPC client; all the intelligence lives in the GitHub Copilot CLI. Before you touch Laravel, confirm the binary works and is authenticated:

copilot --headless --port 12345   # smoke test the server mode

If that fails, nothing downstream will work. Then:

composer require revolution/laravel-copilot-sdk:1.0.22   # pin exactly; surface is still moving
php artisan vendor:publish --tag=copilot-config   # inspect config/copilot.php

Minimal .env for a first run:

COPILOT_CLI_PATH=copilot
COPILOT_MODEL=claude-sonnet-4.6
COPILOT_TIMEOUT=60

Where to look first, in order:

  1. config/copilot.php — binary path, model, timeout, permission_approve, TCP URL. This is your blast radius.
  2. Revolution\Copilot\Facades\Copilot — run(), start(), stream(), client(), fake(), useTcp(), useStdio(), stop().
  3. Revolution\Copilot\Contracts\CopilotSession — sendAndWait(), sendAndStream(), on(), rpc(). Most real work happens here.
  4. Revolution\Copilot\Types\SessionConfig — the single object that controls model, system message, tools, MCP, streaming, folders/permissions, and compaction.
  5. Copilot::client()->rpc() — the namespaces (models(), history(), permission(), plugin(), mode(), plan(), fleet(), skill(), …). Skim the method list once; this is the package's real API and it grew again in 1.0.22.

First use case — a one-shot prompt inside an artisan command, which is the lowest-risk place to evaluate the package:

use Revolution\Copilot\Facades\Copilot;

$response = Copilot::run(prompt: 'Summarize the risks in this migration file.');
$this->info($response->content());

If you get a string back, the transport is healthy. Move to Copilot::start() once you need conversational context. If you get a string back but tools never fire, jump straight to the folder-trust gotcha below.

Implementation Patterns

Session-scoped work is the default for anything user-facing. run() is a fresh session every call; start() gives you a callback where follow-ups retain context:

use Revolution\Copilot\Contracts\CopilotSession;
use Revolution\Copilot\Types\SessionConfig;

$answer = Copilot::start(function (CopilotSession $session) {
    $session->sendAndWait(prompt: 'Read app/Models/Order.php.');
    return $session->sendAndWait(prompt: 'Now suggest a refactor.')->content();
}, config: new SessionConfig(availableTools: ['read_file'], excludedTools: ['shell']));

The availableTools / excludedTools pair is the cheapest guardrail you have and should be set on every agent that isn't fully trusted.

Pin behavior with a system message rather than repeating instructions in prompts. Wrap it in a small factory so every feature shares one persona and one tool allow-list:

function supportConfig(): SessionConfig {
    return new SessionConfig(
        systemMessage: new SystemMessageConfig(content: 'You are a Laravel support assistant. Never invent config keys.'),
        excludedTools: ['shell', 'write_file'],
        streaming: true,
    );
}

Stream to the browser with SSE — this is where the package earns its keep versus a plain text-completion API, because deltas arrive incrementally:

Route::get('/copilot/sse', fn () => response()->eventStream(function () {
    yield from Copilot::stream(function (CopilotSession $session) {
        foreach ($session->sendAndStream('Explain our queue config.') as $event) {
            if ($event->isAssistantMessageDelta()) {
                yield $event->deltaContent();
            }
        }
    }, config: new SessionConfig(streaming: true));
}));

Livewire is the same idea one level down: register on(SessionEventType::ASSISTANT_MESSAGE_DELTA, ...) and call $this->stream(content: $event->deltaContent(), to: 'answer'). Register handlers before sendAndWait() or you'll miss events.

Custom tools are the integration point that makes this more than a chatbot. Give Copilot a scoped, read-only window into your own data:

$config = new SessionConfig(tools: [
    Tool::define(
        name: 'order_status',
        description: 'Look up an order status by ID.',
        parameters: JsonSchema::object([
            'id' => JsonSchema::integer()->description('Order ID')->required(),
        ])->toArray(),
        handler: fn (array $params, array $invocation) => new ToolResultObject(
            textResultForLlm: (string) Order::find($params['id'])?->status,
            resultType: 'success',
            sessionLog: "order_status: {$params['id']}",
            toolTelemetry: [],
        ),
    ),
]);

Keep tool handlers cheap, side-effect-free where possible, and always re-authorize inside the handler — the model's decision to call a tool is not an authorization decision.

Reuse conversations by giving sessions deterministic IDs, keyed to your domain rather than the SDK's random ones:

$config = new SessionConfig(sessionId: "user-{$user->id}-ticket-{$ticket->id}");
Copilot::start(fn (CopilotSession $s) => $s->sendAndWait('Continue.'), config: $config);

Use Copilot::client()->listSessions() when you need to reconcile what exists; use new ResumeSessionConfig() + resume: $id to attach to a session created elsewhere. 1.0.22 adds a host session-withdrawal RPC, so "session existed a minute ago" is no longer a safe assumption — handle a missing session by starting a fresh one instead of throwing.

Drive compaction instead of waiting for it to degrade. 1.0.22 expands the history namespace beyond the old fire-and-forget compact(): you get compaction controls and generated handoff summaries. The pattern worth adopting is a checkpointed session — compact on a threshold, persist the handoff summary as a domain record, and resume from it:

$rpc = Copilot::client()->rpc();

$rpc->history()->compact();                 // prune the live transcript
$summary = $rpc->history()->handoff();      // durable "what happened so far"

Ticket::find($id)->update(['copilot_handoff' => $summary]);

This is also how you hand a session between agents, models, or users without shipping a 200k-token transcript along with it. Treat the handoff summary as a narrative, not a source of truth: anything the next turn must not get wrong belongs in your system message or in a tool lookup, not in prose the model wrote about itself.

Gate permissions explicitly in any web-facing route. The shipped default is deny-all, so a naive integration silently does nothing when the model tries to act. 1.0.22 ports permission prompt context, so the request you receive now carries enough detail to make a real decision rather than guessing from kind:

use Revolution\Copilot\Support\PermissionDecision;

new SessionConfig(onPermissionRequest: function (array $request, array $invocation) {
    // $request now includes prompt context — log it, don't just match on kind.
    Log::info('copilot.permission', $request);

    return match ($request['kind'] ?? null) {
        'shell', 'write' => PermissionDecision::reject(),
        default => PermissionDecision::approveOnce(),
    };
});

Keep the logging. When a user asks "why did it run that?", the prompt context is the only record you'll have.

Use ephemeral UI queries for status, not the transcript. The session UI ephemeral query RPC returns transient state that is deliberately not appended to history. That makes it the right call for a polling status widget or an "is it still thinking?" indicator, and the wrong place to store anything you need later.

Gate plugin installation like a dependency change. 1.0.22 adds verified Agent Plugin installation RPCs. "Verified" is a publisher-identity check, not a code review — treat it as composer require from an unfamiliar vendor. Put installation behind an admin action, record what was installed and when, and never let prompt content trigger an install.

Model switching, plan mode, and cost all live on rpc(). The underused escape hatch now covers more ground than most people realise:

$rpc = Copilot::client()->rpc();

$rpc->mode()->set(new ModeSetRequest(mode: 'plan'));
$plan = $rpc->plan()->read();      // read before you let it act
$rpc->mode()->set(new ModeSetRequest(mode: 'autopilot'));

Exit-plan and auto-mode reverse-RPCs are handled for you as of 1.0.22, so the plan → autopilot handoff no longer needs your own glue. fleet()->start() still gives sub-agent delegation without you writing orchestration. And models()->list() now carries a runtime image-model catalog plus token price units — read them at boot to both validate model identifiers and estimate cost before you fan out.

Fan out with Concurrency for model comparisons or eval runs, since each run() is an independent process:

[$a, $b] = Concurrency::run([
    fn () => Copilot::run('Hello', config: ['model' => 'gpt-5.2'])->content(),
    fn () => Copilot::run('Hello', config: ['model' => 'claude-sonnet-4.6'])->content(),
]);

With price units available from the model catalog, log estimated vs. actual cost per arm — comparisons are the easiest place to accidentally 10x your bill.

Treat long generations as jobs. Wrap Copilot::run() in a queued job with $timeout slightly above COPILOT_TIMEOUT so the worker dies before the RPC does, and broadcast events back to the UI.

Test with fake() and assert on prompts, not on model output. Output is nondeterministic; prompt shape is yours:

Copilot::fake(['*' => Copilot::sequence()
    ->push(Copilot::response('2'))
    ->push(Copilot::response('4'))]);

Copilot::assertPrompt('1 + *');
Copilot::assertPromptCount(2);
Copilot::assertNothingSent();

Gotchas and Tips

  • There are now two silent "nothing happens" gates. permission_approve: 'deny-all' is still the default, and 1.0.22 adds session permission location and folder trust RPCs on top of it. A tool can be allowed by your onPermissionRequest and still never run because the folder isn't trusted for that location. If tools or MCP servers don't fire, check both before you start reading the SDK source. Never flip either to blanket-approve in a route where end users influence the prompt.
  • stdio mode spawns a process per client. That is 200–800ms of overhead plus memory before the model is even called. If you serve more than a trickle of traffic, run copilot --headless --port 12345 and set COPILOT_URL=127.0.0.1:12345. Copilot::useTcp(url: ...) / Copilot::useStdio() let you switch per call.
  • GitHub token switching only works in stdio mode. Copilot::useStdio(array_merge(config('copilot'), ['github_token' => $user->github_token])) gives per-user quota isolation, but you must call Copilot::stop() afterwards to dispose the client, or the next request inherits the previous user's token. In TCP mode there is one shared identity — 1.0.22's login identity selection helps you choose that identity deliberately, but it doesn't make the server multi-tenant.
  • MCP servers are silently ignored without 'tools' => ['*']. The type => 'local' key is also mandatory for local servers. Missing either, the server connects and exposes nothing. MCP OAuth discovery (new in 1.0.22) removes the manual token dance, but not this requirement.
  • No streaming: true, no ASSISTANT_MESSAGE_DELTA events. You'll get exactly one ASSISTANT_MESSAGE at the end and your SSE endpoint will feel broken. Also note reasoning deltas are separate (isAssistantReasoningDelta()) — don't dump them into the user-visible stream by accident. 1.0.22 preserves compaction, tool-usage, and fusion event metadata on those events, so persist the whole event object rather than just deltaContent() if you want a replayable transcript later.
  • Compaction is lossy and now more configurable — which means more ways to get it wrong. A handoff summary that replaces a transcript will drop exact file contents, error strings, and IDs. Anything the next turn must reproduce exactly should be a tool call, not a memory.
  • Content-exclusion path checks are advisory. 1.0.22 adds path checks, but the enforcement you actually rely on is your own: never point a session at a directory containing .env, keys, or customer PII you wouldn't paste into a third-party API. Exclusion lists are a second line of defence, not the first.
  • Model identifiers are a moving target. claude-sonnet-4.6, gpt-5.2, and auto are all valid strings that can rot. Read Copilot::client()->rpc()->models()->list() at boot or in a health check rather than hardcoding, prefer 'auto' where quality variance is acceptable, and use the new price-unit metadata to fail loudly if a model's cost profile changes.
  • Sessions are stateful and ID collisions are real. Two users sharing a sessionId will share transcript history. Always derive IDs from authenticated identity. Withdrawal RPCs mean a resumed session may be gone — treat "resume failed" as a normal branch.
  • Timeouts bite in three places: COPILOT_TIMEOUT, your queue worker timeout, and the HTTP request timeout for SSE. The SSE case is the worst — a default 30s PHP/nginx timeout will cut a stream mid-sentence. Disable output buffering (X-Accel-Buffering: no) for the route. Long compaction or handoff-summary generation counts against the same budget; give it its own job rather than squeezing it into a request.
  • JsonSchema::object() comes from Laravel's JSON schema package, so tool definitions are a hard dependency on a recent Laravel. On older apps you'll be writing array schemas by hand.
  • Experimental is experimental. 1.0.22 ships experimental LLM inference request handling — it is the least stable thing in the changelog. Keep it behind a config flag and out of any path a customer depends on.
  • Extension launch providers and the Laravel AI SDK driver are convenience seams, not stable contracts. If you adopt 'driver' => 'copilot' in config/ai.php, keep the swap path open; the stable seams remain custom tools, onPermissionRequest, MCP servers, skillDirectories/disabledSkills, and the CopilotSession/SessionConfig types.
  • Vendor risk is the honest caveat, and 1.0.22 sharpens it. A ~20-PR release on a low-adoption package is a good sign for maintenance and a bad sign for API stability — nearly all of it is additive RPC surface, which is low-risk to your code but means documentation and your own notes will drift. Pin 1.0.22 exactly in composer.json, isolate calls behind one service class so you can swap in the upstream github/copilot-sdk or a plain API driver, and keep the facade out of your domain layer. The SDK's own CopilotSession contract and SessionConfig types are the right things to depend on if you abstract at all.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource