Commit Graph

3049 Commits

Author SHA1 Message Date
Maximiliano Korp a0ee4cb8e1 style: format files after rebase 2026-08-31 10:46:15 -07:00
Maximiliano Korp 549c472d43 fix(core): preserve recovery state across runtime changes 2026-08-31 10:46:15 -07:00
Maximiliano Korp 714daf8949 fix(runtime): scope channel connection telemetry 2026-08-31 10:46:14 -07:00
Maximiliano Korp 2b56672e32 fix(shared): preserve legacy UI entitlement defaults 2026-08-31 10:46:14 -07:00
Maximiliano Korp 80d2414f9b fix(shared): recognize managed thread limit entitlement 2026-08-31 10:46:14 -07:00
Mike Ryan 7dc04f373d test(angular): complete Core mock 2026-08-31 10:46:14 -07:00
Mike Ryan 32b3166c82 test(angular): isolate entitlement retries 2026-08-31 10:46:13 -07:00
Mike Ryan 88567315c7 docs(runtime): standardize Intelligence project key name 2026-08-31 10:46:13 -07:00
Mike Ryan 034fd234b4 test(web-inspector): use current news telemetry events 2026-08-31 10:46:13 -07:00
Mike Ryan 5c50ba4abe fix(runtime): repair V1 telemetry imports after move 2026-08-31 10:46:13 -07:00
Mike Ryan 2d26a9de1a fix(react-core): isolate assistant inspector context updates 2026-08-31 10:46:13 -07:00
Mike Ryan 0a99ef580a fix(runtime): restore managed authority contracts 2026-08-31 10:46:13 -07:00
Mike Ryan 210c9a0138 fix(runtime): accept App API entitlement responses 2026-08-31 10:46:12 -07:00
Mike Ryan f1ac08938a feat(runtime): use managed Intelligence authority 2026-08-31 10:46:12 -07:00
Tyler Slaton 41efcd55f3 chore: restore main formatting after merge hook 2026-08-31 09:28:50 -07:00
Tyler Slaton d477ca7396 chore: merge main into AG-UI dependency bump 2026-08-31 09:26:33 -07:00
Rainer Hahnekamp a34a7174cf feat(angular): add registerComponent so a component can be a tool the agent calls (refs OSS-1034) (#6773)
## Why

Angular had no counterpart to `useComponent` in
`@copilotkit/react-core/v2` and `@copilotkit/vue/v2`. Letting an agent
display one of your components meant either `registerFrontendTool` with
a handler the component does not need, or `registerRenderToolCall`,
which draws a tool the agent already has and therefore requires that
tool to exist in the agent.

That gap is load-bearing for OSS-1034, which ends the default onboarding
proof at a component rendered through the frontend's own registration.
Every other supported frontend ships `useComponent`; Angular was the
only one that could not express the shape.

## What changed

**`FrontendToolConfig.handler` is now optional**, and
`CopilotKit.#bindClientTool` returns the config unwrapped when it is
absent. This is the actual missing capability. Core already has the
render-only path — a tool that declares no handler gets an empty tool
result and the turn completes (`packages/core/src/core/run-handler.ts`,
the `if (tool.handler)` guards around lines 854 and 1112). Angular bound
a wrapper unconditionally:

\`\`\`ts
handler: (args, context) =>
  runInInjectionContext(injector, () => handler(args, context)),
\`\`\`

so a display-only tool called \`undefined\` on its first tool call. A
stub handler is the wrong fix in the other direction: whatever it
returns becomes the tool's result in the thread, which is a fabricated
result rather than an absent one.

**`registerComponent`** is that shape with the model-facing description
built for it — the same prefix react-core and vue build, so the tool
reads identically to the model whichever frontend registered the
component. It carries no `handler` field at all: a display-only
component that quietly ran application code would be a different
feature, and a caller who wants both wants `registerFrontendTool`.

The component stays an ordinary `ToolRenderer` with a required
`toolCall` signal input reading `toolCall().args`. No new component
contract, and it renders through the existing `pickToolCallHandler` →
`NgComponentOutlet` path unchanged.

Because the tool is declared by the frontend and forwarded over AG-UI, a
display-only component needs nothing added to the agent — on any
framework, Python included.

\`\`\`ts
registerComponent({
  name: "show_incident",
  description: "Show one incident from the incident table.",
  parameters: z.object({ id: z.string(), severity: z.string() }),
  component: IncidentCardComponent,
});
\`\`\`

## Docs

- New `reference/angular/functions/registerComponent.mdx`, wired into
the reference index callout and the `public-api.mdx` summary.
- A display-only section and a new path-table row in the Angular
generative-UI guide.
- `packages/angular/API.md` — the `public-api-documentation.spec.ts`
gate fails on any undocumented export, which is how the omission
surfaced immediately.

The reference page also carries the grounding warning: the model fills
these props from what it knows, so a component rendered over records the
application does not hold looks identical to a correct one in a browser
and in a screenshot.

## Testing

Five tests written before the implementation, confirmed red (`(0 ,
registerComponent) is not a function` ×4, plus the handler-less binding
test) with the 14 pre-existing registration tests still passing, then
green.

- `pnpm nx run @copilotkit/angular:test` — 49 files, 321 passed, 1
skipped.
- `pnpm nx build @copilotkit/angular` — passes, so widening `handler` to
optional broke no caller.
- `showcase/shell-docs` — 66/68 files, 480 tests pass. The 2 failures
are pre-existing on `main` and read files this branch does not touch (a
shared inspector snippet, and mastra tool-rendering content).
- `pnpm run check-format` — the 22 reported files are all pre-existing;
none is in this diff.

Refs OSS-1034

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-31 16:21:47 +02:00
Benjamin Taylor c5107bee53 fix(core): keep the connect-not-implemented escape hatch in the shared pipeline
Self-review catch. The extracted pipeline was faithful to IntelligenceAgent's
copy but not to the base AbstractAgent.connectAgent it replaced on the
self-hosted path, which special-cases one error:

  catchError((error) => {
    this.isRunning = false;
    if (!(error instanceof AGUIConnectNotImplementedError)) {
      return this.onError(input, error, subscribers);
    }
    return EMPTY;
  })

IntelligenceAgent never needed it — it always implements connect() — so the
omission was invisible there. On the SSE path it is load-bearing:
run-handler.ts awaits detachActiveRun() before every run and documents that
this only stops deadlocking because the ConnectNotImplementedError path
reaches the finalize block. Routing it through onError would also fire
run-failure callbacks on every subscriber for a benign condition.

Restores the guard, adds a regression test (verified fail-first: without the
guard connectAgent() rejects with "Connect not implemented"), and matches the
base's `void this.onFinalize(...)`.

Also makes the legacy-chat threadId test actually test its claim. It read
agent.threadId from its own useAgent() call, so the probe performed the very
assignment under test — it passed even with useCopilotChatInternal() removed
entirely. It now reads the agent off the hook's own return value, and is
mutation-checked: disabling the assignment in v2 useAgent fails it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 08:11:21 -05:00
Benjamin Taylor 35616f46a2 fix(core): hydrate self-hosted threads whose replay contains an errored run
A /connect response replays a thread's history, so it can carry several
past runs back to back — including a run that ended in RUN_ERROR followed
by a later RUN_STARTED. The base AbstractAgent connect pipeline runs the
stream through verifyEvents, which enforces single-run lifecycle rules and
rejects that sequence outright:

  Cannot send event type 'RUN_STARTED': The run has already errored with
  'RUN_ERROR'. No further events can be sent.

IntelligenceAgent already omitted verifyEvents from its connect pipeline
for this reason, but self-hosted runtimes (RUNTIME_MODE_SSE) fell through
to super.connectAgent() and so never hydrated such a thread.

Extract that verifyEvents-free pipeline into a shared helper used by both
paths, rather than keeping two copies of a delicate 60-line pipeline.
IntelligenceAgent keeps its canonical-run-id handling and delegates the
rest.

Also adds legacy-chat threadId coverage: the pre-existing connect suite
mocks useAgent wholesale, so nothing exercised the real propagation the
CopilotPopup path depends on.

Fixes #4943

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 08:11:01 -05:00
Benjamin Taylor 37e69105d8 fix(react-core): tear down HITL renderer in the same effect phase it is registered
useFrontendTool now registers the tool renderer in a layout effect, but
useHumanInTheLoop still removed that renderer from a passive effect cleanup.

React runs each phase's cleanups before that phase's effects, but runs the
entire layout phase ahead of the entire passive phase. With the two split
across phases, a keyed remount ordered the outgoing instance's removal after
the incoming instance's registration:

  add (new, layout) -> remove (old, passive)

which deleted the renderer that had just been added and left the HITL tool
unrenderable. Moving the teardown to useLayoutEffect restores the correct
remove-then-add ordering.

Caught by the existing 'should maintain executing state across component
remount' test in use-human-in-the-loop.e2e.test.tsx.
2026-08-31 08:08:48 -05:00
Benjamin Taylor ebbba19101 Merge origin/main into ben1/fe-tool-registry-4952-1746
The v1 react-core tree moved under src/v1-deprecated/, so the two conflicts
were relocations: use-default-tool.ts's DistributiveOmit change re-applied on
the moved file, and the catch-all HITL e2e test moved into
src/v1-deprecated/hooks/__tests__/ with its v2 imports re-rooted.
2026-08-31 07:58:44 -05:00
yannj-fr 661fb1981d fix(react-core): address CodeRabbit findings on the MCP Apps bridge
- Sandbox handshake race: load the ext-apps bridge (dynamic import) BEFORE
  creating and attaching the sandbox iframe. The proxy posts sandbox-proxy-ready
  once during srcdoc execution, and the PostMessageTransport must be listening
  (connect) when it fires. Awaiting the import after the iframe was attached let
  a slow import miss that notification, leaving the widget blank. There is now no
  event-loop yield between attaching the iframe and connecting.
- ui/open-link scheme hardening (XSS): the ext-apps schema validates url as a
  string only, so a widget could pass javascript:/data:/blob: etc. Parse the url
  and refuse a denylist of script-executing / attacker-HTML schemes (javascript,
  data, vbscript, blob, file) before window.open. A denylist is used on purpose so
  custom-scheme deep links (myapp:, whatsapp:, ...) and https universal links keep
  working, since window.open on those hands off to an OS handler rather than
  executing in the page. This matches the Anthropic Software Directory policy
  (https origins + owned custom URI schemes) and the MCP spec's prudent-host
  guidance.
- Tests: reject a disallowed scheme without calling window.open; allow a
  custom-scheme deep link.
2026-08-31 13:30:26 +02:00
yannj-fr f794386528 Merge remote-tracking branch 'upstream/main' into feat/migrate-ext-app-package
# Conflicts:
#	pnpm-lock.yaml
2026-08-31 08:15:25 +02:00
yannj-fr 80d530620c refactor(react-core): make the MCP SDK a hard peer and guard the lazy import
- @modelcontextprotocol/sdk is now a non-optional peerDependency (matching how
  ext-apps declares it) instead of an optional one. ext-apps ships as a direct
  dependency and hard-peers the sdk, so the requirement is already inherited by
  every consumer; the optional flag only hid that and dropped the install-time
  signal. react-core does not use the sdk directly (type import only), so it
  stays out of our dependencies; ext-apps remains the direct dependency.
- Guard the lazy bridge import with a try/catch that rethrows naming the packages
  and the install command, so a missing or version-skewed peer surfaces as an
  actionable error instead of an opaque module-resolution rejection in an effect.
2026-08-31 08:12:12 +02:00
Ben Taylor 69441ccfa4 fix(react-core): render every v1 tool call (#6682)
## What does this PR do?

Fixes the v1 compatibility render path so an assistant message can
render every tool call instead of only `toolCalls[0]`.

The returned lazy renderer now:

- matches each tool call with its corresponding tool result;
- renders all registered tool-call renderers in one fragment;
- removes `null` render results and returns `null` when no tool has a
renderer.

Keeping the fragment behind the existing lazy-renderer callback
preserves the exported `useLazyToolRenderer` return signature. Filtering
before returning also avoids attaching empty generative UI, so
caller-provided subcomponents are not suppressed when no renderer is
registered.

Regression tests cover multiple tool calls, per-call result matching,
the all-unhandled case, and a mixed handled/unhandled message.

## Related PRs and Issues

- Fixes #2946

## Verification

- `pnpm nx run @copilotkit/react-core:test` (133 files, 1,530 Vitest
tests plus 47 script tests)
- `pnpm nx run @copilotkit/react-core:check-types`
- `pnpm exec oxfmt --check
packages/react-core/src/v1-deprecated/hooks/use-lazy-tool-renderer.tsx
packages/react-core/src/v1-deprecated/hooks/__tests__/use-lazy-tool-renderer.test.tsx`

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] Documentation is unchanged because this restores existing v1
behavior without changing the public API
- [x] "Allow edits by maintainers" is checked
2026-08-30 16:59:05 -05:00
Ben Taylor 9909ca44d6 fix(react-core): make agentMetadata.nodeName match the node where the interrupt originates (#6488)
## Problem

`useAgentNodeName` must update React consumers when AG-UI node events
arrive, and `useLangGraphInterrupt.enabled()` must receive the node
where an interrupt actually occurred.

Current `main` includes the basic ref-to-state reactivity fix from
[ffd1580](https://github.com/CopilotKit/CopilotKit/commit/ffd15801d6d),
but that commit explicitly leaves #1426 open: a later `RUN_FINISHED` can
still replace the interrupting node with `"end"`, and v1 consumers can
still be hidden behind `useCoAgent`'s memoized return value.

## What remains in this PR

Rebased onto current `main` (`e9387e0`) after the v1 source migration,
this PR contains only the remaining behavior:

- Preserve the last active node when `RUN_FINISHED` reports `outcome:
"interrupt"`.
- Preserve it for the legacy `on_interrupt` custom-event flow as well.
- Continue transitioning successful and failed runs to `"end"`; reset
new runs and agent switches to `"start"`.
- Add `nodeName` to the `useCoAgent` return-value memo dependencies so
v1 consumers receive the reactive update.
- Share `INTERRUPT_EVENT_NAME` between the hook and interrupt
implementation.

The public hook signatures and AG-UI protocol remain unchanged.

## Preview workflow

- Disabled pkg-pr-new's generated all-package StackBlitz template;
package preview install URLs remain available.

## Changes

- `packages/react-core/src/v1-deprecated/hooks/use-agent-nodename.ts`
- `packages/react-core/src/v1-deprecated/hooks/use-coagent.ts`
-
`packages/react-core/src/v1-deprecated/hooks/__tests__/use-agent-nodename.test.tsx`
- `packages/react-core/src/v2/types/interrupt.ts`
- `packages/react-core/src/v2/hooks/use-interrupt.tsx`
- `.github/workflows/publish-commit.yml`

## Verification

- Full React Core Vitest suite: **131 files, 1520 tests passed**.
- Preview workflow: Nx formatting and YAML parsing passed.
- `nx run @copilotkit/react-core:check-types --skipNxCache`: passed,
including all 33 dependency tasks.
- `git diff --check origin/main...HEAD`: passed.
- The composite React Core test target then reaches the existing
Windows-only script baseline: 8 path-normalization failures plus 2
symlink-permission failures. These are outside this PR's files; the
complete Vitest suite passes before that script stage.

## Scope

This intentionally does not change the AG-UI event protocol, runtime
event ordering, HITL workflow, v1/v2 compatibility layer, or the
separate node tracking in `use-coagent-state-render-bridge.tsx`.

Fixes #1426
2026-08-30 10:05:27 -05:00
Ben Taylor 40beb51634 fix(channels): echo the originating interrupt back as command.interruptEvent (#6353)
## Problem

`thread.resume()` sends `forwardedProps.command = { resume }` and
nothing else.

LangGraph needs nothing more — it correlates a resume by `thread_id` —
so this gap has been invisible. Bridges that correlate by ids carried
**inside the interrupt** cannot work at all. `@ag-ui/mastra` gates its
resume on `command.interruptEvent` and needs `{toolCallId, runId}` out
of it:

```js
let o = input.forwardedProps?.command;
if (!o?.interruptEvent && Array.isArray(input.resume)) { /* standard path */ }
if (o?.resume === false && o?.interruptEvent) { /* cancelled */ }
if (o?.resume != null && o?.interruptEvent) { /* resume */ }
```

With no `interruptEvent`, **every branch is skipped**. The resume is not
an error — it is a silent no-op: the run proceeds as an ordinary new
turn, no `RUN_ERROR`, no tool result, and the suspended tool never
completes. That is the entire Mastra HITL story through Channels today:
the picker appears, the click lands, nothing resumes.

## Change

`Thread` retains the captured interrupt value when the interrupt fires,
and `resume` echoes it back.

- **Opaque.** Channels neither parses nor reshapes the value — it hands
back exactly what the agent sent, so no framework specifics leak in.
This mirrors what `useInterrupt` in `react-core` already does for legacy
interrupts.
- **Durable, not in-memory.** The click that resumes can arrive in a
later run, possibly after a restart — which is the entire reason this
path exists rather than `awaitChoice`.
- **One-use.** Read back with `kv.consume` (atomic take-and-delete),
matching the one-use continuation the resume already claims, so a
replayed click cannot resurrect a spent resume.
- **Retention tied to `actionRetentionMs`**, so the value and the button
that consumes it expire together. Configure a longer action retention
and the value can lapse first; the resume then omits the field, which is
the pre-existing behaviour rather than a new failure.
- **Best-effort store touches.** An agent that needs no correlation data
must not lose its interrupt to a store hiccup.

## Why this is additive

The field is **omitted** when no interrupt value was captured, so the
serialized command is byte-identical for agents that carry no
correlation data, and a normal (non-interrupt) run still writes nothing
at all. LangGraph ignores the extra field.

`interrupt-event-echo.test.tsx` covers all four properties — echo,
durability, one-use, omission. The echo test fails without the fix
(verified by reverting it).

## Verification

- `channels-core`: 268/268 tests, typecheck clean, `oxfmt` clean,
`oxlint` 0 errors.
- Verified end to end against a real Mastra agent driven through Slack:
two full cycles (approve, then decline), each producing a matching
`SUSPENDING` → `RESUMED` pair agent-side. Before the fix the same rig
produced suspends with zero resumes.
- The LangGraph interrupt path was re-checked against the same harness
and is unchanged.

## Reviewer note

One existing assertion in `hitl-continuation.test.tsx` moves from
counting `kv.set` / `kv.consume` calls to matching them by **key**. The
invariant it protects is "an interrupt turn writes exactly one action
snapshot, and one resume consumes it once" — both still asserted, now
stated directly rather than via a total that conflates action state with
unrelated bookkeeping. Flagging it because it is the one pre-existing
test this change edits.

## Out of scope

Channels ignores the standard AG-UI interrupt path (`RUN_FINISHED` with
`outcome.type === "interrupt"`, resumed via a top-level `resume` array).
Mastra emits it today alongside the legacy event, and it carries
interrupt ids and expiry the legacy event lacks. Worth a follow-up.
2026-08-29 14:29:29 -05:00
Ben Taylor 10587a8cd6 chore(channels): drop references to canceled OSS tickets in code comments (#6297)
## What

Three comments in the channels packages point at Linear tickets that
were **canceled on 2026-07-01** (superseded by OSS-401's consolidated
realtime foundation). They read as live tracking pointers when nothing
actually tracks the work.

| File | Reference | Ticket status |
|---|---|---|
| `packages/channels-core/src/codec.ts` | `TODO(OSS-363)` | Canceled |
| `packages/channels-intelligence/src/runtime.ts` | `TODO(OSS-377)` |
Canceled |
| `packages/channels-slack/src/ingress-normalize.ts` | `(OSS-362)`
provenance note | Canceled |

## How

The engineering intent is **preserved** in each case and re-marked as
`TODO (untracked)` with a note that the original ticket was canceled —
so a reader re-files rather than chasing a dead ticket. The `OSS-362`
mention was a bare provenance parenthetical carrying no intent, so it is
simply dropped.

`codec.ts` additionally records that the TODO's stated precondition is
**already met**: the pure Slack ingress mapping (mention stripping,
stable event-id derivation, real-user filtering, field extraction) now
lives in `channels-slack/src/ingress-normalize.ts`. The remaining work
is hoisting `normalizeIngress` onto the generic `PlatformCodec` — worth
knowing for whoever picks it up.

## Why this came up

While grounding a Channels x AG-UI gap analysis, several `OSS-###`
comments turned out to reference closed tickets — three canceled, two
already done. The done ones (OSS-450 Teams managed, OSS-491 delivery
terminal-signal) had already been cleaned out of `main` by subsequent
work; these three canceled ones are what remain.

## Scope / notes

- **Comment-only. No behavior change.** `oxfmt --check` and `oxlint`
both clean.
- Out of scope, flagged for a possible follow-up: `OSS-406`
(`channels-intelligence/src/index.ts`) and `OSS-599`
(`runtime/.../channel-manager.ts`, 5 references) are also canceled, but
sit outside the channels packages surveyed here. Other referenced
tickets (`OSS-96/162/476/566/568/621/623/641/670/691`) were not
status-checked.
2026-08-29 14:28:25 -05:00
Benjamin Taylor f9b306aa4e fix(react-core): register frontend tools before sibling effects run
useFrontendTool registered its tool in a useEffect. React flushes passive
effects child-first in tree order, so a consumer mounted before the
registering component runs its own useEffect against an empty tool list.

That is the cross-page-navigation failure: a page mounts CopilotChat and its
tool-registering components in a single commit, CopilotChat's connect effect
fires first, and the connect request carries no frontend tools.

Register in useLayoutEffect instead. Layout effects run during commit, ahead
of every passive effect regardless of tree order, which closes the window.
This matches useAgentContext, which already registers via useLayoutEffect.

Adds a regression test that mounts the consumer FIRST -- mounting it second
passes with either hook and proves nothing.

Re-derived against current main from mxmzb's #4259, which diagnosed this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 10:32:31 -05:00
Mark 17ed176736 chore: RuleTester coverage for no-single-arg-zod-record lint rule (#5032)
## Summary

Follow-up to #5025 addressing @marthakelly's [review
suggestion](https://github.com/CopilotKit/CopilotKit/pull/5025#discussion_r3307285578):
add oxlint `RuleTester` coverage for the
`copilotkit/no-single-arg-zod-record` rule. Since the underlying Zod 4
incompatibility is **type-level** (no runtime test can catch a
regression), this lint rule is the real safety net, so it's worth
testing directly.

## Cases (via `oxlint/plugins-dev` `RuleTester`)
**valid**
- two-arg `z.record(z.string(), z.unknown())` — no false positive
- two-arg with `.optional()` chain
- single-arg `.record()` on a non-`z` object (`cache.record(entry)`) —
confirms the rule is scoped to the `z` alias and won't over-fire

**invalid**
- single-arg `z.record(z.unknown())` → fires + autofix output
`z.record(z.string(), z.unknown())`
- chained `z.record(z.unknown()).optional()` → fixes the inner call
- `z.record(...spread)` → reports **without** a fix (`output: null`)

## Node version gate (important)
oxlint's `RuleTester` requires **Node ≥ 22** (it throws at parse time on
older runtimes). The CI unit matrix includes a **Node 20** job, so the
cases are gated to skip below Node 22 — verified locally: **6/6 pass on
Node 22**, **skips cleanly on Node 20**. The lint rule itself is still
exercised on every Node version through the `oxlint` job; only these
RuleTester unit tests are gated.

Also extends the `react-ui` vitest `include` to pick up co-located
`oxlint-rules/**/*.test.mjs`.

## Test plan
- [x] `vitest run` on the new file under Node 22 → 6 passed
- [x] same under Node 20 → 1 skipped (gate works; Node 20 CI job stays
green)
- [x] `oxlint` clean on the new test + config

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-29 07:54:58 -07:00
Yann Jouanin cc8aeb95ef Merge branch 'main' into feat/migrate-ext-app-package 2026-08-29 08:43:09 +02:00
Ben Taylor b995233a4d test(shared): add unit tests for conditions engine (#6735)
Add comprehensive unit tests for the xecuteConditions rule engine in
packages/shared/src/utils/conditions.ts, which previously had no test
coverage. Covers all 12 comparison/existence rules, logical AND/OR/NOT
nesting, implicit AND across multiple conditions, and dot-path
resolution (including missing-path and whole-value fallback). Uses the
existing vitest setup in the shared package.
2026-08-28 23:23:22 -05:00
yannj-fr 8f1059d2fa refactor(react-core): address review on the MCP Apps ext-apps migration
- Load the ext-apps bridge lazily. AppBridge/PostMessageTransport now come from a
  dynamic import() inside Effect 1, with a type-only import at the top. A
  <CopilotKit> app no longer pays the ~40-50 kB gzipped ext-apps cost unless it
  actually renders an MCP App. Verified: the built output has no static ext-apps
  import, only import("@modelcontextprotocol/ext-apps/app-bridge").
- Move @modelcontextprotocol/sdk out of dependencies. It is now an optional
  peerDependency (mirroring how ext-apps declares it) plus a devDependency for
  our own build, so the sdk tree (express, hono, jose, ajv, cross-spawn) is no
  longer an install/audit surface for every React consumer. Nothing in the
  bundle reaches sdk at runtime; the only sdk usage is a type import.
- Raise the zod peer floor to >=3.25. The ext-apps/sdk schema slice imports
  zod/v4 and zod/v4-mini, which only exist in zod >= 3.25; the old >=3.0.0 peer
  let a consumer on zod 3.24 hit an unresolvable import at build time.
- Seed the host context at AppBridge construction (hostContext option) instead of
  calling setHostContext after connect, so it is deterministically in place when
  the widget's ui/initialize is handled (the seam #6689 needs to advertise
  displayMode / availableDisplayModes at initialize).
- Restore the full cross-frontend testid surface-contract comment.
- Split the oncalltool guard so "no server hash" and "no agent" report distinctly.
2026-08-28 22:37:39 +02:00
Benjamin Taylor b92821a03c feat(angular): add registerComponent so a component can be a tool the agent calls
Angular had no counterpart to `useComponent` in react-core and vue, so the only
way to let an agent display one of your components was `registerFrontendTool`
with a handler the component does not need, or `registerRenderToolCall`, which
requires the tool to already exist in the agent.

`FrontendToolConfig.handler` becomes optional, and `#bindClientTool` returns the
config unwrapped when it is absent. Core already has the render-only path: a tool
declaring no handler gets an empty tool result and completes the turn. Binding a
wrapper regardless called `undefined` on the first tool call, and a stub handler
would write a fabricated result into the thread instead.

`registerComponent` is that shape with the model-facing description built for it,
identical to the one react-core and vue build, so the tool reads the same to the
model whichever frontend registered the component. The component stays an
ordinary `ToolRenderer` reading `toolCall().args`.

The tool is declared by the frontend and forwarded over AG-UI, so a display-only
component needs nothing added to the agent, on any framework.

Refs OSS-1034

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 15:05:08 -05:00
yannj-fr 21dc441a64 Merge remote-tracking branch 'upstream/main' into feat/migrate-ext-app-package 2026-08-28 21:22:22 +02:00
Aswin Kumar 99d349c6cd Merge branch 'main' into fix/angular-hitl-result-envelope 2026-08-29 00:41:13 +05:30
Benjamin Taylor 4edbbc3f28 fix(react-core): honor chatInputToolbarAddButtonLabel in the add menu tooltip
The add-menu ("+") button's tooltip hardcoded the string "Add attachments",
so `labels.chatInputToolbarAddButtonLabel` only retitled the menu item and
the tooltip stayed English. That blocked full localization of CopilotChat
without replacing the whole add-button slot.

Every other tooltip in the v2 chat surface is already label-driven, and the
Angular implementation already derives this tooltip from the same label, so
this was an oversight rather than a deliberate split.

The "/" shortcut glyph stays hardcoded — it is a key name, not prose.

Fixes #6750

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 13:45:54 -05:00
Ben Taylor d692f25852 feat(runtime): expose Learning Container selector (#6767)
## What does this PR do?

Adds `getLearningContainerId` to `CopilotKitIntelligence` so developers
can assign Intelligence Threads to Learning Containers without a
theta-prefixed Runtime option.

The selector receives:

- The resolved application `user`.
- The parsed AG-UI `input` for the run.
- The `agentId` and the `web` or `channel` surface.

Web runs pass the exact parsed `RunAgentInput`. Channel runs build the
same canonical input that the AgentRunner receives. Channels also carry
the resolved application user through the core and Intelligence adapter
boundaries.

The Runtime validates the selected stable ID and sends only that ID with
the existing Thread create or lock call. Intelligence stays responsible
for project scope, entitlements, Container lookup, and the one-time
Thread binding. Persisted AG-UI events remain the source for Learning
snapshots.

The old `ɵlearning` Runtime option remains as a deprecated fallback. The
Runtime rejects configurations that set both APIs.

## Why?

Learning Container assignment is an Intelligence SDK concern. Developers
also need the resolved user and complete run input to select a Container
from application data without reading raw transport details.

## Related PRs and Issues

- Refs
[ENT-1149](https://linear.app/copilotkit/issue/ENT-1149/enable-projects-to-learn-from-agent-runs-and-publish-reusable-skills)
- Related design: #6746

## Validation

- GitHub CI: 53 passed, 3 skipped
- `pnpm nx run-many -t test,check-types,build -p @copilotkit/runtime
@copilotkit/channels-core @copilotkit/channels-intelligence`
- `pnpm nx run-many -t publint,attw,check-dts -p @copilotkit/runtime
@copilotkit/channels-core @copilotkit/channels-intelligence`
- `pnpm lint` (0 errors; existing warnings remain)
- `npm run typecheck` and `npm run build` in `showcase/shell-docs`
- `npm test` in `showcase/shell-docs` has one pre-existing failure at
`inspector-docs.test.ts:141`: the tracked Threads callout contains
`Playground`.

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] Maintainer edits are available because this PR uses a branch in
the main repository
2026-08-28 13:02:10 -05:00
Ben Taylor 4ccae6fe20 fix(react-native): keep the polyfill imports in the built barrel (closes OSS-1002) (#6744)
## The bug

`src/polyfills.ts` is five side-effect-only imports plus
`installStreamingFetch()`. The published barrel was 195 bytes:

```js
// node_modules/@copilotkit/react-native/dist/polyfills.mjs — 1.69.2
import { t as installStreamingFetch } from "./streaming-fetch-BnQh3vBz.mjs";
installStreamingFetch();
export {  };
```

Every React Native app following the documented setup died on its first
runtime call:

```
E ReactNativeJS: '[CopilotKit] Error (runtime_info_fetch_failed):',
  [ReferenceError: Property 'ReadableStream' doesn't exist]
```

## Root cause

The `sideEffects` field, but not in the way it first looks. It is
correct for *consumers* and wrong for *this package's own build*:

```json
"sideEffects": ["./dist/index.*", "./dist/headless.*", "./dist/polyfills.*", "./dist/polyfills/**/*"]
```

tsdown/rolldown reads the package's own `sideEffects` while bundling and
matches it against **source** paths. `src/polyfills/streams.ts` matches
none of those `dist` globs, so it is declared side-effect-free — a hard
assertion that lets rolldown drop the import without analysing the
`globalThis` assignments inside.

Reproduced in isolation at the pinned tsdown (0.20.3):

| `sideEffects` | built barrel |
|---|---|
| `["./dist/polyfills.*", "./dist/polyfills/**/*"]` | `export { };` —
empty |
| same + `["./src/polyfills.*", "./src/polyfills/**/*"]` | `import
"./polyfills/streams.mjs";` |
| field absent | `import "./polyfills/streams.mjs";` |

**Wider than the ticket recorded:** `dist/index.mjs` and
`dist/headless.mjs` also had zero polyfill code, so the package's
advertised auto-install on first import did not happen either. Not
RN-specific in principle — but I surveyed every package at `origin/main`
and this is the only one exposed. The other `sideEffects` arrays
(`react-core`, `react-ui`, `react-textarea`) are `["**/*.css"]`, which
matches source and works.

## The fix

Add matching `./src/**` globs. Barrel goes 195B → 362B with all five
imports; `headless.mjs` now leads with `import "./polyfills.mjs"`.

## The test, and why the existing one didn't catch this

`src/__tests__/polyfills.test.ts` has ~20 assertions covering all five
groups and was green the whole time — it imports `"../polyfills"`, the
TypeScript **source**, which vitest transpiles without bundling and
therefore without tree-shaking. It exercises a graph the published
package does not contain.

So the new check runs against `dist/`. Two things it has to get right to
be honest:

- **Node ships these globals natively.** Asserting `ReadableStream` is
"defined" after import passes on an empty barrel. The probe clears all
nine first, emulating Hermes.
- **The two formats need different treatment.** CJS is executed for real
in a child realm. ESM is checked structurally — it cannot be executed
here because `encoding.mjs` takes a named import from CommonJS
`text-encoding`, which Metro rewrites to a `require()` but bare Node ESM
rejects.

It is wired into `build`, so a dead barrel fails the build rather than
reaching npm — which matters, because this shipped through a fully green
suite.

## Docs

Added the `Property 'ReadableStream' doesn't exist` symptom to
troubleshooting, which previously covered only the inverse case (a
polyfill *conflict*).

I deliberately left the reference docs' "auto-installs on first import"
claims and the crypto import-order callout alone: both become **true**
once the build is fixed, and I verified the auto-install behaviourally.

## Verification

- **Red/green proven, not assumed:** reverted the `sideEffects` change,
rebuilt → 5/5 groups FAIL in both formats. Restored → 5/5 PASS. There is
also a test for a *single* group regressing, which a whole-barrel
assertion would wave through.
- **Packed tarball** (`pnpm pack`) verified behaviourally: all nine
globals install.
- 289 vitest + 26 script tests pass; `check-types` clean; `attw` green;
`publint` clean apart from a pre-existing `repository.url` suggestion;
oxfmt/oxlint clean.
- Added `{projectRoot}/scripts/**` to the package's `test` inputs and
confirmed cache invalidation (19/19 cached → 18/19 after touching the
verifier); without it, editing the verifier alone would restore a cached
pass.

**Not verified:** the on-device round trip — no emulator in this
environment. The bare-realm equivalent passes on the packed tarball.

## Follow-up worth its own ticket

`dist/polyfills/encoding.mjs` uses a named import from CommonJS
`text-encoding`. Metro handles it; a true-ESM consumer would not.
Pre-existing and not RN-facing, so left out of this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-28 12:24:39 -05:00
Mike Ryan 9e27e10830 feat(runtime): expose Learning Container selector 2026-08-28 10:17:27 -07:00
Ben Taylor 15d4fa0e04 fix(telemetry): stamp sampling metadata and emitter markers on every event (#6749)
Closes OSS-1017, OSS-1018, OSS-1019.

Runtime volume can't currently be counted from the telemetry data alone.
Three separate reasons, all in the two `TelemetryClient`s, all fixed
here.

## What was wrong

**OSS-1017 — v2 samples but doesn't say so.**
`packages/runtime/src/v2/runtime/telemetry/telemetry-client.ts` gates
anonymous events at 5% and lets identified callers through at 100%, then
sends without stamping `sampleRate` / `sampleWeight`. The v1 client has
computed that block for a while; v2 never did. Measured in PostHog
(project 26816, `oss.runtime.copilot_request_created`), the share of
runtime volume arriving unweightable was 1.1% in May, 12.2% in June,
20.2% in July, 24.4% in August — roughly doubling each month. It has
already produced wrong GTM numbers: raw counts understate real volume
10–18× *and* overstate growth (+211% vs a true +128% Jan→Jul), because
the sampled/unsampled mix drifts as v2 adoption rises.

**OSS-1018 — identified v2 events aren't detectable downstream.** The
telemetry id travels as the `X-CopilotKit-Telemetry-Id` header, not an
event property, so whether an event was sampled at 5% or captured at
100% wasn't recoverable from the event. Consumers had to probe for
whatever identity fields happened to be present, which only ~2.7% of v2
events carry.

**OSS-1019 — v1 writes every event twice.** `capture()` sends to both
`lambdaClient.send()` and `segment.track()`, so one request becomes two
PostHog rows with nothing marking them as copies. Both prior dedupe
attempts produced wrong numbers: a "≥ 1.60 dual-emits" version cutoff is
wrong (1.59.5 dual-emits too — it added ~13M phantom requests to June),
and Segment-only drops the entire v2 runtime (19.5M in August).

## What changed

A new `packages/shared/src/telemetry/sampling.ts` holds
`computeSamplingMeta`, and both clients call it. The v1 client's inline
block is replaced by that call with identical output, so the
pre-existing v1 tests act as the regression check that the extraction is
behavior-preserving. v2 passes the result in `globalProperties`, the
slot the sink already spreads into the event and the same one v1 uses —
so both emitters are now identical on the wire.

`telemetry_identified` is carried explicitly rather than inferred from
`sampleWeight === 1`. Under `COPILOTKIT_TELEMETRY_SAMPLE_RATE=1`
anonymous events also weigh 1, and weight alone stops separating the
populations.

For the dual-write, per the issue's option 3 plus a dedupe key —
additive only, both transports keep flowing:

| property | value |
| -- | -- |
| `telemetry_emitter` | `v1-shared` \| `v2-runtime` |
| `telemetry_transport` | `segment` \| `lambda` |
| `telemetry_event_id` | one uuid per v1 `capture()`, identical on both
copies |

v2 doesn't carry an event id — it has a single transport, so there's
nothing to dedupe.

## The counting rule this enables

Drop v1's lambda copy, keep everything else, and sum the stamped weight:

```sql
sumIf(sampleWeight, NOT (telemetry_transport = 'lambda' AND telemetry_emitter = 'v1-shared'))
```

Note for whoever runs the next month-close: **this release breaks the
existing runbook query.** It isolates v2 with `NOT
JSONHas(properties,'sampleRate')`, which matches nothing once v2 starts
stamping `sampleRate` — `v2_runtime` would silently read zero and total
volume would lose ~24%. The runbook has been updated with an
era-agnostic query that gives the same answer either side of the
release; historical events don't get backfilled, so the old inference
path stays as the fallback for pre-fix data.

## Testing

Run from the worktree with `@copilotkit/shared` built so the runtime
resolves the real helper rather than a mock.

**`packages/shared` — 35 passed (35)**, including all 20 pre-existing v1
tests, unchanged. New `sampling.test.ts` (5) covers both branches, the
`sampleRate=1` collision that motivates the explicit flag, and an
empty-string telemetry id. New cases in `telemetry-client.test.ts` (4)
cover both copies sharing one `telemetry_event_id`, a fresh id per
capture, the emitter marker on both copies, and `telemetry_identified`
tracking the gate branch.

```
 ✓ src/telemetry/sampling.test.ts (5 tests)
 ✓ src/telemetry/lambda-client.test.ts (6 tests)
 ✓ src/telemetry/telemetry-client.test.ts (24 tests)
 Test Files  3 passed (3)
      Tests  35 passed (35)
```

**`packages/runtime` v2 telemetry — 37 passed (37)**, covering the new
client suite plus the four pre-existing license/telemetry integration
files:

```
 ✓ src/v2/runtime/telemetry/__tests__/telemetry-client.test.ts (6 tests)
 ✓ src/v2/runtime/telemetry/__tests__/global-properties.test.ts (6 tests)
 ✓ src/v2/runtime/telemetry/__tests__/instance-created.test.ts (5 tests)
 ✓ src/v2/runtime/__tests__/telemetry.test.ts (15 tests)
 ✓ src/v2/runtime/__tests__/sse-license-telemetry.integration.test.ts (1 test)
 ✓ src/v2/runtime/__tests__/sse-license-env-fallback.integration.test.ts (1 test)
 ✓ src/v2/runtime/__tests__/license-telemetry-endpoints.integration.test.ts (3 tests)
      Tests  37 passed (37)
```

One pre-existing assertion changed: `global-properties.test.ts` asserted
`globalProperties` equals `{}` when the caller sets none, which is wrong
by design now that sampling metadata always rides there. Rewritten to
pin the exact key set, which preserves the original intent (nothing of
the caller's is added) and is strictly stronger.

**Mutation-checked** — each new test was verified to fail when its
mechanism is broken, then the mutation reverted and green confirmed:

| mutation | result |
| -- | -- |
| `effectiveSampleRate` always `= sampleRate` (drop identified branch) |
shared 2 failed, runtime 1 failed |
| `telemetry_identified` hardcoded `false` | shared 3 failed, runtime 1
failed |
| v1 event id hoisted per-client instead of per-capture | shared 1
failed |
| both v1 copies stamped `transport: "lambda"` | shared 1 failed |
| v2 drops the sampling block from `globalProperties` | runtime 4 failed
|
| *(reverted)* | shared 35/35, runtime 17/17 |

**Build/lint** — `tsdown` clean for both `@copilotkit/shared` (115
files) and `@copilotkit/runtime` (414 files); `oxfmt` applied; `oxlint`
reports 0 errors. The one warning on new code
(`consistent-function-scoping` on a test-local `jwtWith`) is the same
pattern the existing v1 test file already uses at
`telemetry-client.test.ts:42`.

Not run locally: the pre-commit `test-and-check-packages` hook, which
builds the whole monorepo and fails in this worktree on packages that
were never installed there (`core`, `sdk-js`, `channels-ui`) —
environmental, unrelated to these files. Leaving that to CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-28 12:13:50 -05:00
BenTaylorDev bf1bb98765 chore: release angular v0.4.0 2026-08-28 15:03:28 +00:00
Markus Ecker 71d9731d45 chore(deps): bump @ag-ui/* to 0.0.59
Moves the published packages from 0.0.57 to the current AG-UI release across
@ag-ui/client, core, encoder and proto — 27 declarations in 18 packages.

0.0.59 is the first release carrying the subagent protocol surface
(SUBAGENT_STARTED/FINISHED/ERROR, subagentRunId) along with the null-omission
cleanup, so this is the dependency CopilotKit's subagent work needs.

Scope is packages/** plus the release script noted below. The examples and
showcases sit on a spread of older pins (0.0.40 through 0.0.58) and are left
alone.

One behavioural change comes with the bump. channels-core ships
sanitizeAgentEventStream because @ag-ui/client used to reject a TOOL_CALL_START
carrying parentMessageId: null — the shape @ag-ui/langgraph emits for an
interrupt-triggering tool call. 0.0.59 accepts that null and treats it as
absent, so the two tests asserting the run dies WITHOUT the sanitizer no longer
hold. They now assert the run survives, and the one at agent level still checks
the tool call actually arrives so it cannot pass vacuously. The sanitizer is
untouched and its coercion tests are unchanged; it is simply no longer the
thing keeping such a run alive.

The bump also broke the packed Angular consumer matrix. That job generates a
smoke app from scripts/release/lib/angular-package.ts, whose manifest restated
"@ag-ui/client": "0.0.57" as a literal while packages/angular moved to 0.0.59.
pnpm then installed both copies and the app failed to compile:

  TS2322: Type 'SmokeAgent' is not assignable to type 'AbstractAgent'.
    Types have separate declarations of a private property '_debug'.

The smoke app imports AbstractAgent directly, so it has to resolve the identical
copy the library ships against. Read that version off the packed manifest --
which verify-angular-package.ts already parses for the Angular support contract
-- instead of restating it, so no future AG-UI bump can desynchronise it.
2026-08-28 16:18:52 +02:00
Alem Tuzlak 8469e72b30 feat(web-inspector): copy stored threads into Playground from Threads (#6642)
Inspector Threads now has **Try from here**. One click copies a stored
thread into a Playground scratch session. The stored thread does not
change.

If the copy fails, Inspector stays on Threads and keeps the current
Playground scratch. Example tour threads and locked Threads do not show
the button.

## What does this PR do?

Adds **Try from here** on a real stored thread in Inspector Threads. One
click copies messages and thread state into a Playground scratch
session. The stored thread does not change.

If the copy fails, Inspector stays on Threads and keeps the current
Playground scratch. Example tour threads and locked Threads do not show
the button.

## Related PRs and Issues

- Linear: OSS-873
- Playground base: https://github.com/CopilotKit/CopilotKit/pull/6580
(merged)

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked

## Testing

**Commands run**

1. Rebased `feat/oss-873-try-from-here` onto `origin/main` and resolved
6 conflict files.
2. `npx nx run @copilotkit/web-inspector:test` — 626 tests passed (after
the stale-result guard).
3. `npx nx run @copilotkit/web-inspector:check-types` — passed.

**Manual test**

1. Open Inspector on localhost with Intelligence on, so a real stored
thread exists.
2. Open that thread. Confirm **Try from here** is in the thread header.
3. Click **Try from here**. Confirm Inspector opens Playground with the
copied messages and the stored thread is unchanged.
4. Open an example tour thread. Confirm **Try from here** is not shown.
5. Force a copy failure (disconnect runtime). Confirm Inspector stays on
Threads and the prior Playground scratch is unchanged.

**How this PR makes testing easy**

- `packages/web-inspector/src/__tests__/inspector-navigation.spec.ts`
covers the button, copy path, failure path, and a stale click that must
not overwrite Playground.
- `packages/web-inspector/src/lib/__tests__/telemetry.test.ts` covers
`oss.inspector.threads_try_from_here_clicked`.

## Risk / rollback

Risk is limited to Inspector Threads and Playground. A revert of this PR
removes the button and the new telemetry event. No runtime protocol
change.

## Public API change

New Inspector telemetry export and event name:

**Before**

```ts
trackThreadsTabClicked(props);
```

**After**

```ts
trackThreadsTabClicked(props);
trackThreadsTryFromHereClicked({ ...props, outcome: "success" });
```

`CpkThreadInspector` also emits a `tryFromHere` custom event when the
user clicks the button.
2026-08-28 16:10:06 +02:00
Alem Tuzlak 5a191c8e36 fix(web-inspector): put Try from here next to Expand all
Move the button onto the messages toolbar, on the right of Expand all and Collapse all, with a top-right arrow.
2026-08-28 15:57:18 +02:00
Alem Tuzlak ec146f6721 fix(web-inspector): drop stale Try from here results 2026-08-28 15:01:06 +02:00
Alem Tuzlak d6812e41c8 fix(core): report the runtime connection status from the last actual contact (#6706)
Closes OSS-904.

## The problem

`CopilotKitCore.runtimeConnectionStatus` was set only by the `/info`
handshake, which runs **once on connect**. If the runtime became
unreachable after that, the status stayed `connected` indefinitely —
measured two independent ways against `examples/v2/react/demo` and
recorded in the ticket.

The failure was not lost, it was filed in the wrong drawer: it arrived
as `agent_run_failed`, indistinguishable from an agent bug. So
everything downstream inherited the wrong answer — System Health
reported healthy, the launcher error signal could not fire for the most
common real symptom ("it worked a minute ago"), and a customer `onError`
handler written to separate wiring problems from agent problems got the
wrong classification.

## What this changes

The status now reports **the outcome of the last actual contact with the
runtime**.

A failed runtime request — or silence past a per-request watchdog —
triggers **one** bounded confirmation request. If nothing answers, the
status moves to `error` and the failure is emitted through the existing
wiring error code, so customers already handling startup wiring failures
pick up the mid-session case without changing a line. A subsequent
successful request re-syncs and clears it.

Crucially, **the conversation survives**. The transition does not
discard runtime knowledge, so the agent backing an open chat is the same
instance, its messages stay on screen, and submitting stays possible —
which matters because submitting is what restores the status.

No polling, no heartbeat, no retry loop. Every timer is bound to one
request and dies with it.

## Decisions worth knowing when reviewing

- **Reactive in both directions.** A heartbeat would put permanent
background traffic into every embedding application; a retry loop mostly
races a user who is about to retry anyway. The cost is stated rather
than hidden: while nothing is happening, nothing is detected.
- **Status change is separated from discarding knowledge.** The only
pre-existing code that set `error` also cleared `remoteAgents`. That is
right at startup and destructive mid-session, because conversation state
lives on the agent instance. Four sites now hold this invariant up
together; each carries a comment saying so.
- **The trigger is deliberately permissive, and the check is the
arbiter.** A request that received a successful response never triggers
a check; user cancellation never does; everything else may. Defining the
trigger precisely would mean maintaining a status-code list that is
complete only for the deployment topologies someone thought of.
- **Silence counts.** A server can refuse (fails fast) or hang (accepts
and never answers). A stopped dev server refuses; a container
mid-rollout, a half-switched deploy and a dropped tunnel hang. Only
bounding the check does not help, because no check starts — hence the
per-request watchdog. It observes only and never cancels the request.
- **The rule is stated by destination, not by call site**, so a runtime
route added later inherits the behaviour. Excluded: the Intelligence
realtime endpoint (a different service — reporting its outage as
"runtime unreachable" would be a false diagnosis), endpoints belonging
to the customer, and the stop request.
- **Recovery may prune, under two conditions**: the runtime must have
reported at least one agent, and the agent must carry no conversation
state. An empty list is the signature of a runtime that has not finished
registering.
- **"Answered but refused" keeps the error status and gets a different
message.** An expired token means the app cannot work, so red is right;
telling the reader "unreachable" would send them to check ports and
containers.

## Deliberately not delivered

- Detecting an outage, or a recovery, while the application is idle.
- Recovery by opening the Threads view: every binding withholds thread
requests until the status is already connected, so nothing is sent while
it is red. The thread plumbing still earns its place for *detection*.
- A signal for the Intelligence realtime endpoint failing while the
runtime is healthy — a real gap, and its own ticket.
- Memory and suggestion routes adopting the instrumented fetch.
- A new status value or a new error code.

## Costs this introduces

`error` now means two things — "never connected, no agents" and "lost
mid-session, agents intact". Documented on the enum. And because the
status can now change mid-session at all, an outage costs some churn
that did not exist before: the memory list and the Inspector's thread
list are cleared and refetched, and where the chat owns its run-activity
store it is stopped and restarted. All of it is paid on a user-caused
transition, never while idle.

## Testing

Four independent reviewers audited an earlier revision of this branch;
the ten defects they reproduced are fixed and each is pinned by a test
that was red first. A mutation audit of 110 mutants killed 90; the
surviving holes were closed in the round after.

The connection-health suites carry 72 tests. Request counting is a
first-class assertion throughout, because several decisions are
*absences* — no polling, no retry loop, one check per burst, no traffic
while red — and an absence is only testable by counting. Those tests use
fake timers advancing ten minutes; that boundary is documented where it
lives, since anything slower is invisible to them.

Verified by hand in a browser with the runtime running as its own
process, so the page outlives it: a refusing runtime, a hanging runtime,
recovery, an agent added during an outage, an agent deleted during an
outage. `performance.timeOrigin` was checked throughout to prove the
page never reloaded and the result was not an artefact of a fresh
handshake.

## Follow-ups this leaves behind

Three of these deserve their own ticket. None blocks this PR; all three
are consequences of where its scope was drawn, and they are listed here
so the boundary is explicit rather than implied.

### 1. A signal for the Intelligence realtime endpoint

In Intelligence mode the browser gets its chat events from a **second
service** at its own address; the runtime is only asked for the
credentials. If that service fails while the runtime is healthy, this
change correctly reports the runtime as reachable — and the user
experiences exactly the silence this ticket exists to remove.

It is excluded here on purpose: folding it into the runtime status would
report "runtime unreachable" about a healthy runtime, and a false
diagnosis costs more debugging time than no signal. It needs its own
signal, which is a presentation decision as much as a detection one.

### 2. Memory routes onto the instrumented fetch

The memory store still builds with the global fetch, so its
runtime-bound requests are invisible to connection health. Two costs: a
genuine failure there is a signal we discard, and a success there cannot
restore the status.

The asymmetry is what makes this worth fixing rather than leaving:
memory is the surface most disrupted by a status transition (its list is
cleared and refetched) and currently the one least able to contribute.
The change itself is small — that module already takes its request
function as an injected dependency.

### 3. Consumers should key on what they need, not on the status value

Several consumers treat "status is not connected" as "discard
everything": the memory list, the Inspector's thread list, and the
chat's run-activity store. That was harmless while the status could not
change after page load. It can now, so every outage costs churn that did
not exist before.

This is the same mistake this PR fixes three times *inside* core — a
guard bound to a state instead of to the thing it protects. The
principle was applied internally and not to these consumers. That makes
the churn listed under "Costs" above **deferred rather than inherent**,
and it is the largest of the three follow-ups: three consumers in three
packages, each with its own risk, which is why it was kept out of this
PR.

### Two smaller items

- The launcher error signal on `main` carries a comment stating the
limitation this change removes ("a runtime that dies after the page
loaded … raises nothing … closing that gap means a re-probe in the
core"). It becomes false when this lands and should be corrected then.
- `packages/web-inspector/src/styles/generated.css` is build output
under version control and re-dirties the tree on every build. Unrelated
to this PR, but the Tailwind source glob scans test files, so any prose
comment containing a utility word (`fixed`, `hidden`, `visible`,
`block`) silently changes the committed CSS. Narrowing the glob would
remove the class of problem.

Full specification, including the interview decisions and every
revision: `OSS-904-PRD.md`.
2026-08-28 14:46:15 +02:00
github-actions[bot] c71dcfbb12 style: auto-fix formatting 2026-08-28 14:24:39 +02:00
Alem Tuzlak 95285be33b feat(web-inspector): copy stored threads into Playground from Threads 2026-08-28 14:24:05 +02:00
Alem Tuzlak 1cb76928ad Turn the Home Intelligence card into an install path (#6740)
## Why

The Inspector said what Intelligence *is* and linked out to a signup
page. Of ~1,655 Inspector opens in 90 days, **under 100 clicked any
CTA**. This replaces the feature list with an argument, and the outbound
link with an install that happens in the editor the developer is already
in.

This is the unfinished half of OSS-867, whose body asks for exactly
this: *"If a capability requires Intelligence, detail why and include a
video demonstrating that feature working end-to-end."*

## What changed

**A four-slide argument, paired to the picture beside it.** Each slide
carries two sentences and the visual they describe: your users' threads
→ the pattern inside them → the skill file → what it compounds into. An
earlier draft sold Threads in prose while animating Learning; bound
together they read as one chain, and `meeting-scheduling/SKILL.md`
recurs through all four so the closing diagram is checkable rather than
decorative.

Condensed from the six-phase animation on the Intelligence home page —
not screen-recorded. A ported version is themeable, stays sharp, and
costs no asset weight; the original also runs 21.4s and opens on the
agent booking the wrong meeting, a poor first frame for a card arguing
for the product.

**A copy-prompt button instead of a link out.** It hands the CLI's own
onboarding prompt to a coding agent. Every previous Intelligence CTA
opened a new tab into a signup form, which is where developers drop out.

It carries the CLI's `onboarding_run_id`, so
`oss.inspector.home_prompt_copied` can be joined to
`cli.onboarding.completed` on the Intelligence side. `home_cta_clicked`
only ever proved that someone clicked a link — this is the first event
that can show whether an install followed.

**Section anatomy mirrors System Health** (header band, rule, content),
so the action sits in the same top-right slot the status pill and renew
link already use, and the panel keeps one section shape throughout.

## Correctness of the claims

The copy was checked against the product's own surfaces, and two claims
did not survive:

- **Skills are not applied at run time.** Candidates land at
`pending_review`, a human approves, and the published set is pulled down
with `copilotkit skills download`. Nothing reads published skills during
a run. The slide says approve → pull in → the next run starts from what
worked.
- **Insights were missing**, and with them the evidence link that makes
Learning credible: every Insight cites the Threads and messages behind
it.

Also: *Rich Threads* is the product's name for the durable ones, and the
distinction is the whole sale next to a Threads tab full of local ones
that die on reload. "Your users" means the app's end users — which is
what the platform means too (`identifyUser` resolves one user per
request; a thread carries `end_user_id`, renamed from `user_id` because
the old name *"caused repeated misdiagnosis"*).

## Behaviour worth reviewing

- The story advances **only while Home is visible and the document is
not hidden**. A debugging tool should not hold a repeating timer behind
a closed panel.
- Slide motion is horizontal and derived from each slide's index
relative to the active one, so clicking a tab backwards animates
backwards with no stored direction to fall out of sync.
- Copied state **expires after 4s** so the button invites a second
press; a failed copy **does not**, because that state is the only place
the prompt is selectable by hand.
- Three modes, not two: a lapsed plan keeps the renew link and never
sees an install prompt.
- The rotating copy is hidden from assistive tech (it would announce
four times a loop); one stable sentence is exposed in its place and is
test-covered so it cannot quietly rot.

## Deliberate omissions

**No third-party coding-agent logos on the button**, unlike the
Intelligence app. That app is a private hosted surface; this package
ships inside other people's sites, and vendoring Anthropic's and
OpenAI's marks is not a call to make quietly. The helper line names the
agents in text.

Deferred and worth discussing separately: ordering the Home sections by
state (health first when something is broken, Intelligence first when
nothing is), and putting the same button in the locked Learning and
Threads tabs, where intent is highest.

## Verification

617 tests pass, `check-types` clean, oxlint 0 errors. Verified live in
both themes: all four slides, uniform 16px padding on every slide, card
height stable across slides, copy success **and** failure paths, and the
header band unchanged at 76px when the copied hint appears. The reset
behaviour is mutation-checked — the file records which mutation each
test does and does not catch.
2026-08-28 14:14:39 +02:00