Skip to content

feat(infra): port Runnable interface to TypeScript#1502

Open
SahebSandhuSingh wants to merge 4 commits into
i-am-bee:mainfrom
SahebSandhuSingh:feat/typescript-runnable-interface
Open

feat(infra): port Runnable interface to TypeScript#1502
SahebSandhuSingh wants to merge 4 commits into
i-am-bee:mainfrom
SahebSandhuSingh:feat/typescript-runnable-interface

Conversation

@SahebSandhuSingh

Copy link
Copy Markdown

Summary

Ports Python's Runnable interface (introduced in #971) to TypeScript. This is a prerequisite for the OpenAI-compatible serve adapter requested in #1479, since the issue notes that new server adapters should be built on the unified Runnable interface, and TypeScript currently has no equivalent.

What's included

  • Runnable<R> — abstract class mirroring Python's Runnable[R] ABC, with run(), emitter, and middlewares
  • RunnableOutput — matches Python's RunnableOutput, including the lastMessage getter with its empty-AssistantMessage fallback
  • RunnableOptions{ signal?, context? }, matching Python's RunnableOptions TypedDict
  • runnableEntry — a helper function that wraps RunContext.enter(), used as the TypeScript equivalent of Python's runnable_entry decorator

Design notes / deviations from Python

  • Decorator → helper function: Python uses a @runnable_entry decorator to auto-wrap run(). TS decorators are still experimental/stage-3 and behave inconsistently across build tools, and none of the existing serve adapters in this codebase (A2AServer, MCPServer) use decorators anywhere. A plain helper function (runnableEntry) was used instead, to match the existing code style.
  • extra="allow" on RunnableOutput: Python's pydantic model allows arbitrary extra fields via extra="allow". TS doesn't have a direct equivalent, so an index signature ([key: string]: any) was used as an approximation. Open to feedback on whether there's a more idiomatic pattern already used elsewhere in this codebase for this.
  • Input mutation via middleware: Python's runnable_entry reads the (possibly middleware-modified) input from ctx.run_params rather than the original argument, so middleware can rewrite input before the handler runs. runnableEntry replicates this by reading context.runParams inside the wrapped handler — covered by a regression test.

Testing

  • runnable.test.ts covers:
    • Basic run() flow and output shape
    • lastMessage fallback to an empty AssistantMessage when output is empty
    • Middleware mutating input before the handler runs (regression test for the behavior above)
  • tsc --noEmit passes clean for both files
  • eslint passes clean for both files

Status

Opening as a draft PR for early feedback on direction and naming before going further — happy to adjust based on maintainer input, especially on the RunnableOutput extra-fields approach and whether runnableEntry's signature/naming fits expectations.

@github-actions github-actions Bot added the typescript Typescript related functionality label Jun 17, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the Runnable base class and runnableEntry helper in TypeScript, mirroring the Python implementation to provide a stable interface for unit-of-work execution. The review identified three key areas for improvement: updating the RunnableOutput constructor to correctly handle arbitrary extra fields, adjusting the Runnable constructor's middleware typing to support subclass-specific middlewares, and refining the runnableEntry function signature with generic message types to improve type safety.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread typescript/src/runnable.ts Outdated
Comment on lines +42 to +45
constructor(input: { output: AnyMessage[]; context?: Record<string, any> }) {
this.output = input.output;
this.context = input.context ?? {};
}

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.

medium

The current implementation of RunnableOutput does not assign arbitrary extra fields passed to the constructor, which breaks the intended extra="allow" behavior at runtime and causes TypeScript compilation errors when instantiating with extra properties. Updating the constructor signature and implementation to copy these extra fields resolves this.

  constructor(input: { output: AnyMessage[]; context?: Record<string, any> } & Record<string, any>) {
    this.output = input.output;
    this.context = input.context ?? {};
    const { output, context, ...extra } = input;
    Object.assign(this, extra);
  }

Comment thread typescript/src/runnable.ts Outdated
Comment on lines +66 to +68
constructor(middlewares: MiddlewareType<Runnable<R>>[] = []) {
this._middlewares = middlewares as MiddlewareType<this>[];
}

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.

medium

Typing the constructor parameter as MiddlewareType<Runnable<R>>[] prevents subclasses from passing subclass-specific middlewares due to contravariance of function parameters in TypeScript. Using MiddlewareType<any>[] allows subclasses to safely pass their own middlewares to super().

Suggested change
constructor(middlewares: MiddlewareType<Runnable<R>>[] = []) {
this._middlewares = middlewares as MiddlewareType<this>[];
}
constructor(middlewares: MiddlewareType<any>[] = []) {
this._middlewares = middlewares as MiddlewareType<this>[];
}

Comment thread typescript/src/runnable.ts Outdated
Comment on lines +114 to +119
export function runnableEntry<I extends Runnable<R>, R extends RunnableOutput>(
instance: I,
input: AnyMessage[],
options: RunnableOptions | undefined,
handler: (context: GetRunContext<I>, input: AnyMessage[]) => Promise<R>,
): Run<R, I> {

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.

medium

To preserve the specific message type (e.g., UserMessage[]) passed to runnableEntry instead of widening it to AnyMessage[], introduce a generic parameter M extends AnyMessage for the input messages. This ensures stronger type safety in the handler and the execution context.

export function runnableEntry<
  I extends Runnable<R>,
  R extends RunnableOutput,
  M extends AnyMessage = AnyMessage,
>(
  instance: I,
  input: M[],
  options: RunnableOptions | undefined,
  handler: (context: GetRunContext<I, M[]>, input: M[]) => Promise<R>,
): Run<R, I, M[]> {

Saheb Sandhu added 2 commits June 17, 2026 20:50
Signed-off-by: Saheb Sandhu <sahebsandhu@Sahebs-MacBook-Air.local>
…d update runnableEntry to generic message types

Signed-off-by: Saheb Sandhu <sahebsandhu@Sahebs-MacBook-Air.local>
@SahebSandhuSingh SahebSandhuSingh force-pushed the feat/typescript-runnable-interface branch from 723c699 to 2471240 Compare June 17, 2026 15:20
@SahebSandhuSingh SahebSandhuSingh marked this pull request as ready for review June 17, 2026 16:04
@SahebSandhuSingh SahebSandhuSingh requested a review from a team as a code owner June 17, 2026 16:04
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 17, 2026

@Tomas2D Tomas2D 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.

Thanks @SahebSandhuSingh — the core of this is sound: runnable.ts is clean, purely additive, correctly targets the real RunContext.enter(...) API, and the middleware regression test genuinely exercises the intended behavior. Two things must be cleaned up before merge:

  • Remove the committed package-lock.json (~19.9k lines). This project uses Yarn 4 (Berry) (.yarnrc.yml, packageManager: "yarn@4.9.1"); an npm lockfile must not be committed and will cause dependency drift.
  • Revert the unrelated yarn.lock churn (~2.5k lines of dependency bumps like @ai-sdk/anthropic, @a2a-js/sdk, new @ai-sdk/deepseek). This looks like an accidental npm install — please regenerate with yarn install so the diff is just the two new files.

Also: the interface isn't exported from src/index.ts, so it's currently unreachable to consumers — please add the export (or note it's intentionally internal for now). Once the lockfiles are reverted and the fork-gated CI passes, the source itself looks good.

@SahebSandhuSingh

Copy link
Copy Markdown
Author

Ok @Tomas2D I will do the needful

Signed-off-by: Saheb Sandhu <sahebsandhu@Sahebs-MacBook-Air.local>
@SahebSandhuSingh

Copy link
Copy Markdown
Author

@Tomas2D Addressed all three:

  • Removed package-lock.json
  • Reset yarn.lock to match upstream/main and reinstalled — diff is clean, no unrelated dependency churn
  • Exported Runnable from src/index.ts

Shall i start with OpenAI-compatible server adapter for TypeScript??

@SahebSandhuSingh SahebSandhuSingh requested a review from Tomas2D July 5, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files. typescript Typescript related functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants