Skip to content

feat(think): lifecycle hooks, dynamic context, extension manifest#1278

Merged
threepointone merged 4 commits intomainfrom
even-more-think
Apr 9, 2026
Merged

feat(think): lifecycle hooks, dynamic context, extension manifest#1278
threepointone merged 4 commits intomainfrom
even-more-think

Conversation

@threepointone
Copy link
Copy Markdown
Contributor

@threepointone threepointone commented Apr 9, 2026

Summary

Think now owns the inference loop end-to-end. Instead of overriding onChatMessage() for full control, developers use lifecycle hooks that fire at every stage of the agentic loop. Session gains dynamic context blocks for runtime context management. The extension manifest is expanded with context block declarations and hook support.

Phase 1 — Own the inference loop

  • Removed onChatMessage(), assembleContext(), getMaxSteps() — Think owns streamText internally via private _runInferenceLoop(TurnInput). All 4 entry paths (WebSocket, chat(), saveMessages, auto-continuation) converge on it.
  • New lifecycle hooks:
    • beforeTurn(ctx) — inspect assembled system prompt, messages, tools, model. Return TurnConfig to override any part (model, system, messages, tools, activeTools, toolChoice, maxSteps, providerOptions).
    • beforeToolCall(ctx) — fires when model calls a tool (observation only for now).
    • afterToolCall(ctx) — fires after tool execution with result.
    • onStepFinish(ctx) — per-step callback.
    • onChunk(ctx) — per-chunk callback (streaming analytics).
  • maxSteps is now a property (default 10), not a method. Per-turn override via TurnConfig.maxSteps.
  • MCP tools auto-merged — no need to manually merge this.mcp.getAITools().
  • ToolCallDecision is a discriminated union: allow | block | substitute.

Phase 2 — Dynamic context + extension manifest

  • Session.addContext(label, options?) — register context blocks after session initialization.
  • Session.removeContext(label) — remove dynamically added blocks.
  • ContextBlocks.addBlock() / removeBlock() — underlying primitives.
  • ExtensionManifest expanded with:
    • context — namespaced context block declarations ({extName}_{label})
    • hooks — lifecycle hooks the extension provides
  • Bridge providers (ExtensionContextBridge, ExtensionSkillBridge, ExtensionWritableBridge) — Phase 4 infrastructure for extension Worker RPC delegation.
  • _initializeExtensions() in Think — creates ExtensionManager, loads static + restored extensions, registers context blocks in Session, wires unload cleanup.
  • extensionLoader property + extensionManager field on Think.

Docs + example

  • README rewritten with new "Configuration" and "Lifecycle hooks" tables, beforeTurn example, TurnConfig docs, dynamic context section.
  • Assistant example updated: maxSteps property, removed manual MCP merging, added beforeTurn/onChatResponse hooks.

Breaking changes

  • onChatMessage() removed — use lifecycle hooks. Full-custom inference: extend Agent directly.
  • assembleContext() removed — absorbed by beforeTurn. Think assembles internally; hook sees defaults and can override.
  • getMaxSteps() method removed — use maxSteps property.
  • ChatMessageOptions deprecated — aliased to TurnInput.

These are expected for an @experimental 0.x package. No other packages in the repo are affected — all examples using AIChatAgent are untouched.

Test plan

  • 200 tests pass across 8 test files
  • All existing tests migrated (6 test agents updated)
  • Phase 1: 11 hook tests (beforeTurn context, multi-turn, convergence, onStepFinish, onChunk, maxSteps)
  • Phase 2: 10 dynamic context tests (add/remove, prompt integration, writability, tool generation, coexistence)
  • Build succeeds for both agents and think packages
  • No other packages or examples broken (verified: all Think subclasses checked)
  • agents package changes are purely additive (new methods only)

Known limitations

  • beforeToolCall blocking/substitution is not yet functional — the AI SDK's streamText does not expose a pre-execution interception point in the Workers runtime. The hook fires post-execution (observation only). Types (ToolCallDecision with block/substitute) are in place for future implementation.
  • Extension manifest context.type is declared but not enforced — all blocks use SQLite-backed storage until bridge providers are wired (Phase 4).

Made with Cursor


Open with Devin

…essage

Phase 1 of the Think extension system redesign. Think now owns the
streamText call end-to-end, enabling lifecycle hooks at every stage
of the agentic loop. Users who need full custom inference extend
Agent directly instead of overriding onChatMessage.

BREAKING CHANGES:

- Remove onChatMessage() — Think owns the streamText call internally
  via private _runInferenceLoop(TurnInput). All 4 entry paths
  (WebSocket, chat(), saveMessages, auto-continuation) converge on it.

- Remove assembleContext() — absorbed by beforeTurn hook. Think
  assembles context internally; beforeTurn receives the result in
  TurnContext and can override any part via TurnConfig.

- Remove getMaxSteps() method — replaced by maxSteps property
  (default 10). Per-turn override via TurnConfig.maxSteps.

- Deprecate sanitizeMessageForPersistence() — will move to session
  configuration in a future release.

- Deprecate ChatMessageOptions — aliased to TurnInput for migration.

NEW LIFECYCLE HOOKS:

- beforeTurn(ctx: TurnContext) → TurnConfig | void
  Fires before streamText. Inspect assembled system prompt, messages,
  tools, model. Return overrides: model, system, messages, tools,
  activeTools, toolChoice, maxSteps, providerOptions.

- beforeToolCall(ctx: ToolCallContext) → ToolCallDecision | void
  Fires when model produces a tool call. Currently observation-only
  (fires via onStepFinish data post-execution). ToolCallDecision is
  a discriminated union: allow | block | substitute. Block/substitute
  not yet functional — AI SDK doesn't expose pre-execution interception
  in Workers runtime. Types are in place for future implementation.

- afterToolCall(ctx: ToolCallResultContext) → void
  Fires after tool execution with tool name, args, and result.

- onStepFinish(ctx: StepContext) → void
  Fires after each step (initial, continue, tool-result) with step
  type, text, tool calls, tool results, finish reason, usage.

- onChunk(ctx: ChunkContext) → void
  Fires per streaming chunk. High-frequency, observational only.

NEW CONFIGURATION:

- maxSteps property (replaces getMaxSteps method)
- getExtensions() stub for Phase 2 extension declaration
- MCP tools auto-merged into tool set (no manual merging needed)
- waitForMcpConnections moved inside inference loop

NEW EXPORTED TYPES:

TurnInput, TurnContext, TurnConfig, ToolCallContext, ToolCallDecision,
ToolCallResultContext, StepContext, ChunkContext, ExtensionConfig

DESIGN DECISIONS:

- _runInferenceLoop is private — hooks always fire, no bypass possible
- ToolCallDecision is a discriminated union (allow/block/substitute)
  with clear, non-overlapping semantics per action
- chat() now wraps _runInferenceLoop in agentContext.run() for
  consistency with the WebSocket path
- _transformInferenceResult is a protected test seam for error
  injection (replaces the old onChatMessage stream-wrapping pattern)

TEST CHANGES:

- Migrated 6 test agents that overrode onChatMessage or getMaxSteps
- TestAssistantAgentAgent: replaced fake stream with mock model
- ThinkTestAgent: error injection via _transformInferenceResult,
  added beforeTurn/onStepFinish/onChunk instrumentation
- ThinkProgrammaticTestAgent: captures via beforeTurn instead of
  onChatMessage
- ThinkRecoveryTestAgent/ThinkNonRecoveryTestAgent: count via
  beforeTurn instead of onChatMessage
- LoopToolTestAgent: added tool call hook instrumentation
- ThinkToolsTestAgent: switched to tool-calling mock model
- Renamed _onChatMessageCallCount → _turnCallCount
- Updated stale test descriptions referencing removed methods
- Added 11 new hook tests (hooks.test.ts): beforeTurn context,
  multi-turn, convergence across entry paths, onStepFinish,
  onChunk, maxSteps property
- 191 tests pass across 9 test files

Made-with: Cursor
Phase 2 of the Think extension system redesign. Extensions can now
declare context blocks in their manifests, and Session supports
dynamic add/remove of context blocks after initialization.

SESSION CHANGES (packages/agents):

- ContextBlocks.addBlock(config) — register a new context block after
  init. Triggers load() if blocks haven't been loaded yet. Initializes
  provider, loads content, adds to both configs array and blocks Map.

- ContextBlocks.removeBlock(label) — remove a block. Cleans up from
  configs, blocks, and loaded skills tracking. Skill unload callbacks
  are NOT fired (appropriate for full extension removal). Caller must
  call refreshSystemPrompt() to rebuild the prompt.

- Session.addContext(label, options?) — public API wrapping addBlock.
  Auto-wires AgentContextProvider (SQLite-backed) when no provider is
  given. Requires builder-constructed sessions (Session.create).

- Session.removeContext(label) — public API wrapping removeBlock.

EXTENSION MANIFEST (packages/think):

- ExtensionManifest.context — array of context block declarations
  with label, description, type (readonly/writable/skill/searchable),
  and maxTokens. Labels are namespaced as {extName}_{label}.
  Type is declared but not yet enforced (all blocks use SQLite
  storage until bridge providers are implemented in Phase 4).

- ExtensionManifest.hooks — lifecycle hooks the extension provides.

- ExtensionInfo.contextLabels — namespaced labels in list() output.

BRIDGE PROVIDERS (packages/think — Phase 4 infrastructure):

- ExtensionContextBridge, ExtensionWritableBridge, ExtensionSkillBridge
  adapt extension Worker RPC into Session provider interfaces.
  Uses protected base fields so children don't duplicate state.
  Not wired yet — current blocks use AgentContextProvider directly.

- createBridgeProvider(label, type, entrypoint) factory function.

EXTENSION LIFECYCLE IN THINK:

- _initializeExtensions() — creates ExtensionManager from
  extensionLoader property, loads static extensions from
  getExtensions(), restores dynamic extensions from DO storage,
  registers extension context blocks in Session via addContext()
  (SQLite-backed, not bridge-delegated), wires onUnload callback.

- extensionLoader property — set to env.LOADER to enable extensions.

- extensionManager field — public, auto-created when extensionLoader
  is set. Use for dynamic load()/unload() at runtime.

- Extension tools auto-merged in _runInferenceLoop.

- ExtensionManager.unload() fires onUnload callback which removes
  context blocks from Session and refreshes the system prompt.

- ExtensionManager.onUnload(cb) — register cleanup callback.
- ExtensionManager.getContextLabels() — namespaced labels.
- ExtensionManager.getManifest(name) — get manifest by name.

HIBERNATION RESTORATION:

onStart() ordering:
  1. Workspace initialization
  2. configureSession() (builder phase)
  3. ExtensionManager created (if extensionLoader set)
  4. getExtensions() loaded (static extensions)
  5. restore() (dynamic extensions from DO storage)
  6. Extension context blocks registered in Session (mutation phase)
  7. Protocol handlers
  8. User's onStart()

TESTS:

5 new tests for dynamic context:
  - addContext registers a new block
  - addContext block appears in system prompt after refresh
  - removeContext removes the block
  - removeContext returns false for non-existent block
  - removed block disappears from system prompt after refresh

195 total tests pass across 8 files.

Made-with: Cursor
README:
- Replace "Override points" table with "Configuration" + "Lifecycle hooks"
  tables reflecting the new API (no onChatMessage, assembleContext, getMaxSteps)
- Add beforeTurn example with TurnConfig documentation
- Add "Dynamic context blocks" section showing addContext/removeContext
- Update MCP section to note auto-merging
- Update production features list with lifecycle hooks

Assistant example:
- Replace getMaxSteps() with maxSteps property
- Remove manual MCP tool merging in getTools() (auto-merged now)
- Add beforeTurn and onChatResponse hooks to demonstrate the lifecycle
- Import new types (TurnContext, TurnConfig, ChatResponseResult)

Changeset:
- Patch for @cloudflare/think and agents
- Documents breaking changes (removed onChatMessage, assembleContext,
  getMaxSteps) and new features (hooks, maxSteps property, MCP
  auto-merge, dynamic context blocks, expanded manifest)

Made-with: Cursor
Add 5 more tests for dynamic context blocks:

- dynamic block is writable by default (AgentContextProvider)
- dynamic block content can be written via setContextBlock
- session tools include set_context after adding writable block
- addContext coexists with configureSession blocks (both in prompt)
- dynamic block visible in chat turn tools (negative: ThinkTestAgent
  has no context blocks, so set_context should not appear)

Also add test helpers to ThinkSessionTestAgent:
- getSessionToolNames() — returns tool names from session.tools()
- getContextBlockDetails() — returns writable/isSkill for a block

200 total tests pass across 8 files.

Made-with: Cursor
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: eddb62d

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

This PR includes changesets to release 2 packages
Name Type
@cloudflare/think Patch
agents 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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 9, 2026

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1278

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1278

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1278

hono-agents

npm i https://pkg.pr.new/hono-agents@1278

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1278

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1278

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1278

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1278

commit: eddb62d

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 7 additional findings.

Open in Devin Review

@threepointone threepointone merged commit 8c7caab into main Apr 9, 2026
2 checks passed
@threepointone threepointone deleted the even-more-think branch April 9, 2026 10:12
@github-actions github-actions bot mentioned this pull request Apr 9, 2026
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.

1 participant