Skip to content

Define the function middleware error contract: propagation, recoverable tool errors, and fatal failures #107

Description

@shibayan

Summary

The function middleware error model diverges from .NET/Python in two coupled places and must be decided as one contract.

  1. The innermost TypeScript handler catches a tool-body exception and stores it as ctx.error, so it does not unwind through middleware await next().
  2. An ordinary exception thrown by middleware fails the whole run. Python now distinguishes an ordinary recoverable exception from an explicit fatal MiddlewareFailure and user-input suspension.

Decision

Record separately:

  • whether a tool-body exception unwinds through the middleware onion; and
  • which middleware-thrown exceptions are recoverable function errors versus fatal run failures.

If the Python-style fatal type is adopted, service-managed conversations also need settlement of every in-flight local call so a hosted thread is not left with dangling calls.

Acceptance criteria

Contract decision

  • Both decisions above are recorded with rationale; this issue is not closed after deciding only one.
  • Public API documentation and types match the choice, including the fate of FunctionMiddlewareContext.error.
  • MiddlewareTerminated and UserInputRequiredError retain their existing termination/suspension semantics.
  • Any breaking middleware contract ships in a minor release with migration notes.

If the Python-style contract is adopted

  • A tool-body exception unwinds through await next(), so middleware can observe or replace it with try/catch.
  • If no middleware recovers, the outer invocation boundary emits exactly one function error result and increments the consecutive-error budget once.
  • An ordinary exception thrown by middleware before or after next() becomes exactly one recoverable function error result.
  • A public fatal exception equivalent to Python MiddlewareFailure rejects the run and is never folded into ctx.error or a normal result.
  • Awaited/streamed and sequential/concurrent batches have equivalent outcomes and no call executes twice.

Fatal failure settlement

  • When a service-managed conversation already contains an in-flight local call batch, a fatal middleware failure makes one best-effort settlement request before rethrowing the original error.
  • Settlement contains one error function_result for every dangling local call and uses toolChoice: "none".
  • The session/continuation advances to the settlement response ID; other settlement output is discarded.
  • Settlement failure never masks the original fatal error, and no settlement request is made without provider-managed state.
  • Approved-tool replay is covered in awaited and streamed modes.

If the current contract is retained

  • Tests pin that tool failures return through next() as ctx.error, ordinary middleware exceptions fail the run, and post-next() middleware executes after a tool failure.
  • The retained divergence and recovery recipe are documented for middleware authors.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

coreUsage: [Issues, PRs], Target: packages/core

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions