Skip to content

feat(deepagents): reload skills when skillsMetadata is set to null - #862

Merged
Richard Scarrott (richardscarrott) merged 8 commits into
mainfrom
feat/skills-reload-on-null
Sep 17, 2026
Merged

Richard Scarrott (richardscarrott) merged 8 commits into
mainfrom
feat/skills-reload-on-null

Conversation

@richardscarrott

Copy link
Copy Markdown
Contributor

Summary

With a checkpointer, a thread loads skills once and keeps them in skillsMetadata. Skills added, edited or deleted in the backend after that never reach the model. There was no supported reset. updateState with null failed validation, the reducer ignored [], and a new list merged by name, so deleted skills stayed.

Set skillsMetadata to null between runs, and the next run reloads every source and replaces the stored list:

await agent.updateState(config, { skillsMetadata: null });
// or as part of the next run's input
await agent.invoke({ messages, skillsMetadata: null }, config);
  • skillsMetadata is now a plain .nullish() field with no reducer and no default.
  • beforeAgent loads when the value is null or missing. A stored [] now means the sources had no skills, so those threads stop listing the backend on every run. That includes existing checkpoints that store [].
  • skillsMetadataReducer is gone. No package entry point exported it.

This is the same contract as langchain-ai/deepagents#6288, with null in place of None.

Closes #562

Why removing the reducer is safe

  • fix(deepagents): use new StateSchema class #145 added the reducer and the [] default because parallel subagents returned skillsMetadata and hit "LastValue can only receive one value per step". The reducer ignored empty updates because those subagents returned empty lists.
  • fix: missing files in skillsMiddleware schema #160 switched the load check to "non-empty list", because the [] default made a != null check always true.
  • feat(deepagents): support skills in subagents #187 excluded skillsMetadata from subagent state in both directions. That removed the reason for the reducer, but the reducer stayed.
  • Nothing else writes the key. Forks still receive the parent's list, and no subagent hands a list back.
  • A new test sends a general-purpose task call and a fork in one message and checks the parent's list afterwards. With skillsMetadata taken out of EXCLUDED_STATE_KEYS, it fails with INVALID_CONCURRENT_GRAPH_UPDATE. That test keeps the removal safe if someone edits the exclusion list.

Rolling back after a reset

If a thread is reset and deepagents is then downgraded before that thread runs again, agent.invoke/agent.stream on the older version throws Validation failed for field "skillsMetadata", because older versions don't accept null. To recover, run agent.updateState(config, { skillsMetadata: [] }) on the older version. LangGraph API server deployments aren't affected.

Docs: langchain-ai/docs#6043


Supersedes #860, which is the same work on a branch in my fork (richardscarrott/deepagentsjs). Identical commits — this branch is bd23287, the commit hntrl approved there, pushed unchanged.

Moved here because CodeQL is on GitHub's default setup, which doesn't analyse fork PR refs, while the main ruleset requires CodeQL results. #860 had 0 CodeQL runs and sat at Code scanning is waiting for results from CodeQL, unsatisfiable rather than merely pending. Origin branches get analysed, so the requirement can actually be met here.

Review discussion and approval live on #860.

Setting `skillsMetadata` to `null` between runs, with `updateState` or in
the next run's input, makes the next run reload every skill source and
replace the stored list. A stored `[]` now counts as loaded.

Removes the unused `skillsMetadataReducer`, since subagents don't return
`skillsMetadata`.
`createDeepAgent({ skills: [...] })` mounts `SkillsMiddleware` at runtime, but
the agent's state type was built only from the unconditionally mounted
middleware, so `skillsMetadata` was missing from `invoke` and `stream` inputs.
Reaching it meant mounting `createSkillsMiddleware` by hand.

Capture `skills` as a const type parameter and splice the skills middleware
into the inferred state when it is present. Inference mirrors the runtime
condition: an omitted option or an empty literal contributes no state, while a
non-literal `string[]` does.

Widen `SkillsMiddlewareOptions["sources"]` and `SubAgent["skills"]` to
`readonly string[]`, which the const type parameter requires and which also
lets callers pass readonly arrays. Export `SkillsMiddleware` and
`SkillMetadataEntry`.

`updateState` is deliberately left untested: LangGraph types its values
parameter as `unknown`, so any assertion there would pass regardless.
A middleware can only read and write the fields declared on its own state
schema, so a middleware that wanted to inspect `skillsMetadata`, or set it
to `null` to force a reload, had no way to declare the field: returning
`{ skillsMetadata: null }` without it fails to compile.

Export `skillsMetadataValue` for that declaration, and have
`SkillsStateSchema` consume it so the field has one definition. It stays a
plain nullish array with no reducer: a `ReducedValue` would accept more than
one write per step, which would make the parallel-subagent guard in
`subagent.test.ts` vacuous.

The value is documented as opaque. It exists to be passed to `StateSchema`,
so keeping its concrete type unpinned leaves room to swap it later without
breaking callers. `SkillMetadataEntrySchema` stays internal; the
`SkillMetadataEntry` type is the public surface for a single entry.

Also adds the missing `SkillMetadataEntry` to the browser entry point, which
the previous commit added to `index.ts` only.
`v24.x` is not a version mise can resolve ("semver range 24.x is not
supported"), which breaks the node and pnpm shims locally. `24` resolves
for mise and is equally acceptable to `actions/setup-node` via
`node-version-file`.
Loading in `beforeAgent` pinned `skillsMetadata` for a whole run, so a
reload requested by setting it to `null` could not be served until the
next run. Loading in `beforeModel` serves it on the next model call,
which lets a middleware of your own invalidate mid-run — from
`afterModel`, say — and have the following call see the fresh list.

This also matches Python's `SkillsMiddleware`, which has always loaded in
`before_model` with the same "`null` means reload, `[]` means loaded and
empty" guard.

Two further consequences. Resolving a `StateBackend` now sees `files`
written during the run, so a `SKILL.md` the agent just wrote is picked
up. And a fork spawned in the same iteration that nulled the field
inherits the `null`, so it reloads from its own sources — which are the
parent's, by construction.

`wrapModelCall` keeps collapsing `null` into `[]`, as Python does. That
leaves one gap: `jumpTo: "model"` from an `afterModel` hook routes
straight to the model node and bypasses `beforeModel`, so a pending
reload waits an extra iteration and that one call is told there are no
skills. `humanInTheLoopMiddleware` jumps this way on a rejected tool
call. Fixing the jump target is a separate change.
Co-authored-by: Hunter Lovell <40191806+hntrl@users.noreply.github.com>
Signed-off-by: Richard Scarrott <riscarrott@googlemail.com>
Co-authored-by: Hunter Lovell <40191806+hntrl@users.noreply.github.com>
Signed-off-by: Richard Scarrott <riscarrott@googlemail.com>
@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd23287

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
deepagents Minor
deepagents-acp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
deepagentsjs-ui Ready Ready Preview Sep 17, 2026 9:21am UTC

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Sep 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

deepagents-acp

npm i https://pkg.pr.new/deepagents-acp@862

deepagents

npm i https://pkg.pr.new/deepagents@862

@langchain/sandbox-standard-tests

npm i https://pkg.pr.new/@langchain/sandbox-standard-tests@862

@langchain/daytona

npm i https://pkg.pr.new/@langchain/daytona@862

@langchain/deno

npm i https://pkg.pr.new/@langchain/deno@862

@langchain/modal

npm i https://pkg.pr.new/@langchain/modal@862

@langchain/node-vfs

npm i https://pkg.pr.new/@langchain/node-vfs@862

@langchain/quickjs

npm i https://pkg.pr.new/@langchain/quickjs@862

commit: bd23287

@open-swe open-swe Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/deepagents/src/agent.ts
Comment thread libs/deepagents/src/agent.ts
@richardscarrott
Richard Scarrott (richardscarrott) merged commit 6e6b036 into main Sep 17, 2026
34 checks passed
@richardscarrott
Richard Scarrott (richardscarrott) deleted the feat/skills-reload-on-null branch September 17, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills gets cached in checkpoint and does not reflect after new skill is uploaded

2 participants