In the run, add a page to connect to the agent through the browser #11
Labels
No labels
ai::retry
ai::retry_once
ai::retry_until_green
ai::validate_plan
status::blocked
status::done
status::functional_analysis
status::in_ci
status::in_development
status::in_review
status::technical_analysis
status::waiting_info
status::waiting_validation
type::analysis
type::bug
type::question
type::task
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Alchemic.Studio/Alfred#11
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
In the page of run's
/runs/{id}, add a buton to a page to show the current screen of the running agent when the run useclaude code,codex, etc...We want to see the agent running live to follow what it doe
Functional analysis
Expected behaviour
The run detail page
/runs/{id}gets a button that opens a separate page,/runs/{id}/live, in the same tab. That page shows a live, readable activity feed of what the agent in the run is doing, so a user can follow it as it works.Decisions from the discussion:
/runs/{id}can open the live page. Anyone who cannot see the run cannot open it./runs/{id}/live), reached from a button on the run page.Acceptance criteria
/runs/{id}shows a button that leads to/runs/{id}/live. This holds for every run, whatever the agent type (command-line agent or direct LLM API) and whatever the runner (container or host)./runs/{id}/livewithout a manual page reload, in the order they happened./runs/{id}can open/runs/{id}/live. A user who is not allowed gets the same denial as on the run page, and no activity content leaks.Out of scope
Thanks @SebastienDenooz, I am on it. I will post my analysis here.
I need a few answers before going further. Reply in a comment; without an answer by 2026-11-04 15:49 UTC, I will apply the defaults.
q1 — What should the "live screen" show? Right now each command-line agent (Claude Code, Codex, OpenCode, Copilot, Grok Build) runs in non-interactive mode. Its output is collected only when it finishes, so no terminal screen exists while it works.
q2 — Should the browser view be watch-only, or can the user also act on the agent?
q3 — Which runs show the button?
q4 — What happens to the page when the run is not running (still pending, or already succeeded, failed or cancelled)?
q5 — Who can open the live page? It may show code, prompts and command output from the run's sandbox.
/runs/{id}(default)q6 — Where does the live view open?
/runs/{id}/live) reached from a button on the run page (default)q7 — Does the agent have to run in a container, or must the live view also work when it runs directly on the worker host (the "host" runner)?
You can also answer every question at once in a comment of your own.
Ok, i setup the answer on q3 and all others are ok for me. You can continue with that.
Plan
Live activity page for runs (
/runs/{id}/live)What exists today
routes/web.php:88-90andapp/Http/Controllers/RunController.php.GET /runs/{run}/liveis already taken by the auto-refreshing fragment of the run page (runs.live→runs/partials/live.blade.php, polled byresources/js/live.js). The issue requires that URL for the new page, so the fragment moves (see step 6).app/Llm/Local/LocalAgentAdapter::invoke()makes one blockingContainerRuntime::exec()call, and stdout is only read when it returns. Containers useProcess::run()inapp/Sandbox/CliContainerRuntime.php:89. The host runner usesProcess::start()plus a polling loop inapp/Sandbox/HostRuntime.php:190.--json), OpenCode (--format json), Copilot (--output-format json, JSON lines) and Grok Build (streaming-json) already print one JSON event per line as they work.--output-format json, which prints a single object at the end.app/Llm/Api/ApiLlmAdaptercalls$agent->prompt()once. Each tool call goes throughToolContext::run(), which records aToolCallRecord. These records are written tollm_tool_callsonly after the run, inRunExecutor::recordResult().app/Jobs/PurgeRetention.php. Secret masking isapp/Logging/Redactor.php. Browser tests use the Pest browser plugin (tests/Browser/..., Playwright under the hood).Approach
1. Storage: a
run_activitiestablecreate_run_activities_tablewith these columns:id,organization_id,run_id(FK, cascade on delete),llm_session_id(nullable)sequence(unsigned int, unique together withrun_id)kind(string)title(string ≤ 255: the tool name, the file path, or the command)body(text, nullable)occurred_atrun_id,sequence).App\Models\RunActivityusing theBelongsToOrganizationconcern, plus a factory.App\Enums\ActivityKindwith the valuesmessage,tool_call,tool_result,file_read,file_edit,command,error. Each value has alabel()used for the badge, which covers criterion 3 ("labelled by kind").App\Llm\Data\ActivityEntry(readonly: kind, title, body).2. Recording:
App\Runs\Activity\ActivityRecorderRunExecutor(run id, session id, organization id) and passed to the adapter in a new optionalLlmRequest::$activity(?ActivitySink, a small interface withrecord(ActivityEntry ...$entries)).SmokeTestpasses nothing.Redactor, and the run's credentials are registered as secrets too.bodyis cut toalfred.llm.activity_excerpt_chars(default 4000).alfred.llm.activity_max_entries(default 2000) and writes one finalerrorentry saying the feed was truncated.RunExecutor::invokeInRunner()flushes the recorder in afinallyblock.3. Live output from the runners
?Closure $onOutput = null(receives stdout chunks) as the last parameter ofContainerRuntime::exec(). Pass it through inRunnerRuntimeandNoContainerRuntime.CliContainerRuntime:$pending->run($argv, fn ($type, $chunk) => $type === 'out' && $onOutput?->__invoke($chunk)).HostRuntime: inside the existingwhile ($process->running())loop, forward$process->latestOutput(), then drain what remains after the loop.FakeContainerRuntime: scripted results can declare chunks that are sent to the callback before the run returns.ExecResultkeeps holding the full stdout, so text, outcome, usage and exit code are worked out exactly as today.4. Turning CLI events into entries
LocalAgentAdapter:App\Llm\Local\JsonLineStream) splits chunks into complete lines and decodes each JSON event.activity(array $event): list<ActivityEntry>(default[]) is called for each event, and its result goes to$request->activity.extractText(). These adapters already depend on each CLI's event format.assistanttext → message.tool_use→ Read / Glob / Grep becomefile_read, Edit / Write / MultiEdit becomefile_edit, Bash becomescommand.tool_result→ result.item.completedwithagent_message/command_execution/file_change/reasoning.text,tool_useparts.assistant.message, tool events.textchunks merged into one message per run of text, plustool_call/tool_call_update.--output-format stream-json --verbose.extractText()andusage()read the final{"type":"result"}line, which has the same fields as today's single JSON object.rawOutput(ExecResult)(default: stdout) returns only that result line for Claude Code.raw_outputtherefore keeps its current shape (criterion 11).mapOutcome()and the auth/rate-limit patterns still read the full output.5. API runs
ToolContextreceives the activity sink.run()records atool_callentry: tool name, a summary of the arguments (path, command…), mapped tofile_read/file_edit/commandby tool name.run()records atool_resultentry with the result excerpt and success.laravel/aidocs (context7) for per-step events or the agent middleware / step callbacks.$response->stepswhenprompt()returns. The tool calls stay live.llm_tool_callswrite inrecordResult()is unchanged.6. Routes, controller, views
GET /runs/{run}/fragment(runs.fragment). Updateshow.blade.php:32and its tests.GET /runs/{run}/live→RunController@live(runs.live): the full page.GET /runs/{run}/live/feed?after={sequence}→RunController@feed(runs.live.feed): an HTML fragment containing only the entries after the cursor, plus the run state.CurrentOrganization::abortUnlessOwns()andGate::authorize('view', $run), the same checks asshow(criterion 9: same 404 / redirect to login).resources/views/runs/live.blade.php: back link to the run, the state badge, and an ordered list<ol data-feed>.resources/views/runs/partials/feed.blade.php: entries as<li data-sequence data-kind>, each with a kind badge, a title, and the body in an escaped<pre>(never raw HTML).data-live-finished="true"is set.show.blade.php: a "Watch live" link next to "Re-run", shown for every run (criterion 1).resources/js/live.jswith an append mode, so the existing replace mode is unaffected.data-live-mode="append"requesturl?after=<last data-sequence>and append the returned<li>s.[data-live-finished="true"]appears.7. Retention and docs
PurgeRetentionsetsrun_activities.body = null(andtitlefor commands and files) for purged sessions, in the same transaction.docs/specification.md§27.2, §27.5 and §27.6 (new routes, renamed fragment, activity feed, retention).Tests
*AdapterTest:activity()mapping from recorded JSON-line samples. Claude CodeextractText()/usage()/rawOutput()on stream-json output, with the result unchanged compared to today.JsonLineStream: partial lines across chunks.ActivityRecorder: redaction, truncation, max entries, batched flush, errors swallowed.CliContainerRuntimeTest/HostRuntimeProcessTest: the callback receives chunks before the process ends, and theExecResultis unchanged.FakeContainerRuntimeTest: scripted chunks.ToolContext: live entries for calls and results.tests/Feature/Admin/Runs/RunLiveTest.php:aftercursor returns only newer entries.data-live-finishedflag.RunScreensTestupdated for/fragment.RunExecutortests withFakeContainerRuntime: a local run and an API run, each on both the container and the host runner, write activity rows and leavetext_output/raw_output/outcomeunchanged.PurgeRetentiontest.tests/Browser/Runs/RunLiveTest.php./runs/{id}/live..github/workflows/ci.ymland.forgejo/workflows/ci.yml(and.gitlab-ci.yml). Check that the new test directory is picked up.Risks
stream-jsonneeds--verbosewith-p, and the new flag must work with the supported version range. Check this againstclaude --helpfor 2.x.Redactor, and purged with the rest.laravel/aiexposes no step hook. Tool calls stay live either way.show.blade.phpand the tests use it.The implementation plan is ready for validation. Add the ai::validate_plan label to let me start.
Plan validated by @SebastienDenooz. Starting the development.
The status label status::in_ci is managed by me: I restored it. Use the command labels to steer my work.