feat(infra): port Runnable interface to TypeScript#1502
feat(infra): port Runnable interface to TypeScript#1502SahebSandhuSingh wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
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.
| constructor(input: { output: AnyMessage[]; context?: Record<string, any> }) { | ||
| this.output = input.output; | ||
| this.context = input.context ?? {}; | ||
| } |
There was a problem hiding this comment.
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);
}| constructor(middlewares: MiddlewareType<Runnable<R>>[] = []) { | ||
| this._middlewares = middlewares as MiddlewareType<this>[]; | ||
| } |
There was a problem hiding this comment.
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().
| constructor(middlewares: MiddlewareType<Runnable<R>>[] = []) { | |
| this._middlewares = middlewares as MiddlewareType<this>[]; | |
| } | |
| constructor(middlewares: MiddlewareType<any>[] = []) { | |
| this._middlewares = middlewares as MiddlewareType<this>[]; | |
| } |
| 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> { |
There was a problem hiding this comment.
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[]> {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>
723c699 to
2471240
Compare
Tomas2D
left a comment
There was a problem hiding this comment.
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.lockchurn (~2.5k lines of dependency bumps like@ai-sdk/anthropic,@a2a-js/sdk, new@ai-sdk/deepseek). This looks like an accidentalnpm install— please regenerate withyarn installso 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.
|
Ok @Tomas2D I will do the needful |
Signed-off-by: Saheb Sandhu <sahebsandhu@Sahebs-MacBook-Air.local>
|
@Tomas2D Addressed all three:
Shall i start with OpenAI-compatible server adapter for TypeScript?? |
Summary
Ports Python's
Runnableinterface (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 unifiedRunnableinterface, and TypeScript currently has no equivalent.What's included
Runnable<R>— abstract class mirroring Python'sRunnable[R]ABC, withrun(),emitter, andmiddlewaresRunnableOutput— matches Python'sRunnableOutput, including thelastMessagegetter with its empty-AssistantMessagefallbackRunnableOptions—{ signal?, context? }, matching Python'sRunnableOptionsTypedDictrunnableEntry— a helper function that wrapsRunContext.enter(), used as the TypeScript equivalent of Python'srunnable_entrydecoratorDesign notes / deviations from Python
@runnable_entrydecorator to auto-wraprun(). 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"onRunnableOutput: Python's pydantic model allows arbitrary extra fields viaextra="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.runnable_entryreads the (possibly middleware-modified) input fromctx.run_paramsrather than the original argument, so middleware can rewrite input before the handler runs.runnableEntryreplicates this by readingcontext.runParamsinside the wrapped handler — covered by a regression test.Testing
runnable.test.tscovers:run()flow and output shapelastMessagefallback to an emptyAssistantMessagewhen output is emptytsc --noEmitpasses clean for both fileseslintpasses clean for both filesStatus
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
RunnableOutputextra-fields approach and whetherrunnableEntry's signature/naming fits expectations.