You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#133 fixes the model-facing text path for MCP tool results by correctly extracting text from MCP content blocks instead of stringifying the raw result.
The important invariant is that text transformations such as truncation must not accidentally destroy structured output unless that behavior is intentional.
For example:
truncate text to 5,000 chars
should not automatically imply:
discard structuredContent
Model Consumption
This issue should preserve structured content first.
It does not necessarily need to define the final long-term strategy for how every model consumes structured tool results.
Possible future approaches include:
Text serialization
Provide a deterministic JSON representation to the model:
{
"invoices": [...]
}
Native structured tool messages
Use provider-supported structured tool result capabilities where available.
Selective exposure
Keep structured data in runtime state while sending only selected fields/text to the model.
The initial implementation should avoid locking Extra into one model-provider-specific strategy.
Artifacts
MCP / LangChain tool results may also carry artifacts in addition to text.
text == "Found 2 invoices"
structured == {"count": 2}
Multiple text blocks + structured content
All text blocks remain normalized correctly while structured output is preserved unchanged.
Structured-only result
Define and test behavior when structured output exists but no useful text content exists.
For example:
{
"structuredContent": {
"balance": 1250
}
}
Extra should not silently discard the result.
Local string tool
return"done"
continues to behave exactly as today.
Local structured tool
If supported by the normalization abstraction, verify structured Python output can also be preserved without MCP-specific code leaking into the runtime.
Idempotent replay
tool executed
→ structured result persisted
same tool call replayed
→ no provider call
→ identical normalized result restored
Hook transformation
A text transformation does not accidentally discard structured output.
Unsupported/non-text MCP block
Non-text content is handled deterministically and does not destroy accompanying structured data.
Acceptance Criteria
MCP structuredContent is no longer silently discarded.
throws away information the runtime already received.
Extra should preserve the original structured result so future layers can decide how to use it:
model
UI
agent orchestration
evaluation
replay
workflow logic
The core invariant should be:
A tool execution boundary may normalize provider-specific result formats, but it should not silently destroy structured information returned by the tool.
Support MCP
structuredContentand Artifacts in Tool ResultsContext
Follow-up to #133.
#133 fixes the model-facing text path for MCP tool results by correctly extracting text from MCP content blocks instead of stringifying the raw result.
That solves cases like:
{ "content": [ { "type": "text", "text": "Balance: $1,250" } ] }so the model receives:
instead of a Python representation of the content block.
However, MCP tools may also return structured output in addition to text.
For example:
{ "content": [ { "type": "text", "text": "Found 2 invoices" } ], "structuredContent": { "invoices": [ { "id": "INV-123", "amount": 500, "currency": "USD" }, { "id": "INV-456", "amount": 800, "currency": "USD" } ] } }Today Extra normalizes the text content for the model, but the structured result is not preserved through the execution pipeline.
Problem
The current tool execution contract effectively reduces every successful tool result to:
strConceptually:
This is sufficient for plain textual tools, but lossy for MCP tools that expose structured output.
For example:
{ "content": [ { "type": "text", "text": "Found 2 invoices" } ], "structuredContent": { "invoices": [...] } }becomes only:
The actual invoice objects are discarded from Extra's runtime representation.
This limits several capabilities:
Goal
Preserve MCP tool results as structured runtime data while keeping the existing model-facing text behavior intact.
Extra should distinguish between:
and:
instead of collapsing both into a single string.
Conceptually:
Suggested Runtime Model
Instead of treating a successful tool result as only:
strintroduce a generic normalized result object.
Conceptually:
The exact naming/API should fit the existing runtime architecture.
The important point is that tool execution should preserve both:
independently.
MCP Example
Given an MCP tool result:
{ "content": [ { "type": "text", "text": "Found 2 invoices" } ], "structuredContent": { "invoices": [ { "id": "INV-123", "amount": 500 }, { "id": "INV-456", "amount": 800 } ] } }Extra should preserve something equivalent to:
The model may still initially receive:
if that remains the current model contract.
But the structured value should remain available to the runtime.
Local Tool Compatibility
This should not become MCP-specific throughout the engine.
Local tools may also eventually return structured data.
For example:
The normalization boundary should therefore ideally be generic:
rather than:
MCP-specific parsing belongs at the provider/adapter boundary.
The rest of Extra should consume a provider-agnostic result representation.
Relationship to
ToolMessageLangChain may expose MCP structured output through the tool result /
ToolMessageartifact path.The implementation should inspect the actual adapter contract rather than assuming all result data lives in:
Conceptually:
Extra should extract the MCP-specific structured result from the appropriate field and normalize it into Extra's own runtime representation.
The Extra domain/runtime layer should not depend on LangChain-specific result objects after normalization.
Persistence
The current
ToolExecutionManagerpersists the final result as text for idempotent replay.If structured results are introduced, replay must preserve the same logical result.
For example:
It would be incorrect if:
because execution behavior would depend on whether the call was executed live or restored from the ledger.
The persisted execution result should therefore evolve accordingly.
Hooks
The current
transform_tool_resulthook receives text.We should explicitly decide how structured data interacts with hooks.
Possible direction:
or a normalized result object:
The important invariant is that text transformations such as truncation must not accidentally destroy structured output unless that behavior is intentional.
For example:
should not automatically imply:
Model Consumption
This issue should preserve structured content first.
It does not necessarily need to define the final long-term strategy for how every model consumes structured tool results.
Possible future approaches include:
Text serialization
Provide a deterministic JSON representation to the model:
{ "invoices": [...] }Native structured tool messages
Use provider-supported structured tool result capabilities where available.
Selective exposure
Keep structured data in runtime state while sending only selected fields/text to the model.
The initial implementation should avoid locking Extra into one model-provider-specific strategy.
Artifacts
MCP / LangChain tool results may also carry artifacts in addition to text.
For example:
Artifacts may represent:
Extra should preserve artifact metadata where it is meaningful and safe.
However, large binary payloads should not automatically be copied into:
Prefer preserving references/metadata rather than blindly serializing arbitrary binary data.
Security and Data Handling
Structured tool output may contain sensitive business data.
Preserving it must not mean automatically logging it.
The existing runtime already avoids logging raw tool arguments/results in several paths.
The same principle should apply here.
Structured results must not be automatically emitted into:
unless explicitly configured.
The normalized result should remain application/runtime data.
Failure Semantics
Malformed structured output should not corrupt an otherwise valid textual tool response.
For example:
should have an explicitly defined policy.
Possible behavior:
rather than converting the entire successful tool execution into a failure.
If the MCP tool declares an output schema and the structured output violates it, validation behavior should be defined separately and explicitly.
Suggested Flow
Scope for v1
structuredContent.Tests
Text + structured content
Input:
{ "content": [ { "type": "text", "text": "Found 2 invoices" } ], "structuredContent": { "count": 2 } }Expected:
Multiple text blocks + structured content
All text blocks remain normalized correctly while structured output is preserved unchanged.
Structured-only result
Define and test behavior when structured output exists but no useful text content exists.
For example:
{ "structuredContent": { "balance": 1250 } }Extra should not silently discard the result.
Local string tool
continues to behave exactly as today.
Local structured tool
If supported by the normalization abstraction, verify structured Python output can also be preserved without MCP-specific code leaking into the runtime.
Idempotent replay
Hook transformation
A text transformation does not accidentally discard structured output.
Unsupported/non-text MCP block
Non-text content is handled deterministically and does not destroy accompanying structured data.
Acceptance Criteria
structuredContentis no longer silently discarded.Non-Goals
Those can build on top of the normalized result abstraction later.
Why This Matters
MCP tools are not only text generators.
A tool may return precise machine-readable data such as:
{ "invoice_id": "INV-123", "amount": 500, "currency": "USD" }Reducing that result to:
throws away information the runtime already received.
Extra should preserve the original structured result so future layers can decide how to use it:
The core invariant should be: