Skip to content

Commit f0c027c

Browse files
authored
refactor: tighten internal types and eslint fixes (assistant-ui#1911)
1 parent 509a0f4 commit f0c027c

File tree

8 files changed

+122
-63
lines changed

8 files changed

+122
-63
lines changed

packages/assistant-stream/src/core/tool/ToolCallReader.ts

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@ import {
88
ToolCallReader,
99
ToolCallResponseReader,
1010
} from "./tool-types";
11-
import { TypeAtPath, TypePath } from "./type-path-utils";
11+
import { DeepPartial, TypeAtPath, TypePath } from "./type-path-utils";
1212
import { ToolResponse } from "./ToolResponse";
13+
import {
14+
asAsyncIterableStream,
15+
AsyncIterableStream,
16+
ReadonlyJSONObject,
17+
ReadonlyJSONValue,
18+
} from "../../utils";
1319

1420
// TODO: remove dispose
1521

16-
function getField<T>(obj: T, fieldPath: (string | number)[]): any {
17-
let current: any = obj;
22+
function getField<T>(obj: T, fieldPath: (string | number)[]): unknown {
23+
let current: unknown = obj;
1824
for (const key of fieldPath) {
1925
if (current === undefined || current === null) {
2026
return undefined;
2127
}
22-
current = current[key as string | number];
28+
current = current[key as keyof typeof current];
2329
}
2430
return current;
2531
}
@@ -29,14 +35,14 @@ interface Handle {
2935
dispose(): void;
3036
}
3137

32-
class GetHandle<T> implements Handle {
33-
private resolve: (value: any) => void;
38+
class GetHandle<T, TValue> implements Handle {
39+
private resolve: (value: TValue) => void;
3440
private reject: (reason: unknown) => void;
3541
private disposed = false;
3642
private fieldPath: (string | number)[];
3743

3844
constructor(
39-
resolve: (value: any) => void,
45+
resolve: (value: TValue) => void,
4046
reject: (reason: unknown) => void,
4147
fieldPath: (string | number)[],
4248
) {
@@ -58,7 +64,7 @@ class GetHandle<T> implements Handle {
5864
) {
5965
const value = getField(args as T, this.fieldPath);
6066
if (value !== undefined) {
61-
this.resolve(value);
67+
this.resolve(value as TValue);
6268
this.dispose();
6369
}
6470
}
@@ -74,12 +80,12 @@ class GetHandle<T> implements Handle {
7480
}
7581

7682
class StreamValuesHandle<T> implements Handle {
77-
private controller: ReadableStreamDefaultController<any>;
83+
private controller: ReadableStreamDefaultController<unknown>;
7884
private disposed = false;
7985
private fieldPath: (string | number)[];
8086

8187
constructor(
82-
controller: ReadableStreamDefaultController<any>,
88+
controller: ReadableStreamDefaultController<unknown>,
8389
fieldPath: (string | number)[],
8490
) {
8591
this.controller = controller;
@@ -118,13 +124,13 @@ class StreamValuesHandle<T> implements Handle {
118124
}
119125

120126
class StreamTextHandle<T> implements Handle {
121-
private controller: ReadableStreamDefaultController<any>;
127+
private controller: ReadableStreamDefaultController<unknown>;
122128
private disposed = false;
123129
private fieldPath: (string | number)[];
124-
private lastValue: any = undefined;
130+
private lastValue: string | undefined = undefined;
125131

126132
constructor(
127-
controller: ReadableStreamDefaultController<any>,
133+
controller: ReadableStreamDefaultController<unknown>,
128134
fieldPath: (string | number)[],
129135
) {
130136
this.controller = controller;
@@ -165,13 +171,13 @@ class StreamTextHandle<T> implements Handle {
165171
}
166172

167173
class ForEachHandle<T> implements Handle {
168-
private controller: ReadableStreamDefaultController<any>;
174+
private controller: ReadableStreamDefaultController<unknown>;
169175
private disposed = false;
170176
private fieldPath: (string | number)[];
171177
private processedIndexes = new Set<number>();
172178

173179
constructor(
174-
controller: ReadableStreamDefaultController<any>,
180+
controller: ReadableStreamDefaultController<unknown>,
175181
fieldPath: (string | number)[],
176182
) {
177183
this.controller = controller;
@@ -182,7 +188,7 @@ class ForEachHandle<T> implements Handle {
182188
if (this.disposed) return;
183189

184190
try {
185-
const array = getField(args as T, this.fieldPath) as unknown as any[];
191+
const array = getField(args as T, this.fieldPath);
186192

187193
if (!Array.isArray(array)) {
188194
return;
@@ -226,10 +232,12 @@ class ForEachHandle<T> implements Handle {
226232
}
227233

228234
// Implementation of ToolCallReader that uses stream of partial JSON
229-
export class ToolCallArgsReaderImpl<T> implements ToolCallArgsReader<T> {
235+
export class ToolCallArgsReaderImpl<T extends ReadonlyJSONObject>
236+
implements ToolCallArgsReader<T>
237+
{
230238
private argTextDeltas: ReadableStream<string>;
231239
private handles: Set<Handle> = new Set();
232-
private args: any = parsePartialJsonObject("");
240+
private args: unknown = parsePartialJsonObject("");
233241

234242
constructor(argTextDeltas: ReadableStream<string>) {
235243
this.argTextDeltas = argTextDeltas;
@@ -268,8 +276,12 @@ export class ToolCallArgsReaderImpl<T> implements ToolCallArgsReader<T> {
268276
get<PathT extends TypePath<T>>(
269277
...fieldPath: PathT
270278
): Promise<TypeAtPath<T, PathT>> {
271-
return new Promise<any>((resolve, reject) => {
272-
const handle = new GetHandle<T>(resolve, reject, fieldPath);
279+
return new Promise<TypeAtPath<T, PathT>>((resolve, reject) => {
280+
const handle = new GetHandle<T, TypeAtPath<T, PathT>>(
281+
resolve,
282+
reject,
283+
fieldPath,
284+
);
273285

274286
// Check if the field is already complete in current args
275287
if (
@@ -281,7 +293,7 @@ export class ToolCallArgsReaderImpl<T> implements ToolCallArgsReader<T> {
281293
) {
282294
const value = getField(this.args as T, fieldPath);
283295
if (value !== undefined) {
284-
resolve(value);
296+
resolve(value as TypeAtPath<T, PathT>);
285297
return;
286298
}
287299
}
@@ -291,11 +303,13 @@ export class ToolCallArgsReaderImpl<T> implements ToolCallArgsReader<T> {
291303
});
292304
}
293305

294-
streamValues<PathT extends TypePath<T>>(...fieldPath: PathT): any {
306+
streamValues<PathT extends TypePath<T>>(
307+
...fieldPath: PathT
308+
): AsyncIterableStream<DeepPartial<TypeAtPath<T, PathT>>> {
295309
// Use a type assertion to convert the complex TypePath to a simple array
296310
const simplePath = fieldPath as unknown as (string | number)[];
297311

298-
const stream = new ReadableStream<any>({
312+
const stream = new ReadableStream<DeepPartial<TypeAtPath<T, PathT>>>({
299313
start: (controller) => {
300314
const handle = new StreamValuesHandle<T>(controller, simplePath);
301315
this.handles.add(handle);
@@ -315,15 +329,19 @@ export class ToolCallArgsReaderImpl<T> implements ToolCallArgsReader<T> {
315329
},
316330
});
317331

318-
// For type compatibility, cast the stream to the required type
319-
return stream as any;
332+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
333+
return asAsyncIterableStream(stream) as any;
320334
}
321335

322-
streamText<PathT extends TypePath<T>>(...fieldPath: PathT): any {
336+
streamText<PathT extends TypePath<T>>(
337+
...fieldPath: PathT
338+
): TypeAtPath<T, PathT> extends string & infer U
339+
? AsyncIterableStream<U>
340+
: never {
323341
// Use a type assertion to convert the complex TypePath to a simple array
324342
const simplePath = fieldPath as unknown as (string | number)[];
325343

326-
const stream = new ReadableStream<any>({
344+
const stream = new ReadableStream<unknown>({
327345
start: (controller) => {
328346
const handle = new StreamTextHandle<T>(controller, simplePath);
329347
this.handles.add(handle);
@@ -343,15 +361,19 @@ export class ToolCallArgsReaderImpl<T> implements ToolCallArgsReader<T> {
343361
},
344362
});
345363

346-
// For type compatibility, cast the stream to the required type
347-
return stream as any;
364+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
365+
return asAsyncIterableStream(stream) as any;
348366
}
349367

350-
forEach<PathT extends TypePath<T>>(...fieldPath: PathT): any {
368+
forEach<PathT extends TypePath<T>>(
369+
...fieldPath: PathT
370+
): TypeAtPath<T, PathT> extends Array<infer U>
371+
? AsyncIterableStream<U>
372+
: never {
351373
// Use a type assertion to convert the complex TypePath to a simple array
352374
const simplePath = fieldPath as unknown as (string | number)[];
353375

354-
const stream = new ReadableStream<any>({
376+
const stream = new ReadableStream<unknown>({
355377
start: (controller) => {
356378
const handle = new ForEachHandle<T>(controller, simplePath);
357379
this.handles.add(handle);
@@ -371,12 +393,12 @@ export class ToolCallArgsReaderImpl<T> implements ToolCallArgsReader<T> {
371393
},
372394
});
373395

374-
// For type compatibility, cast the stream to the required type
375-
return stream as any;
396+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
397+
return asAsyncIterableStream(stream) as any;
376398
}
377399
}
378400

379-
export class ToolCallResponseReaderImpl<TResult>
401+
export class ToolCallResponseReaderImpl<TResult extends ReadonlyJSONValue>
380402
implements ToolCallResponseReader<TResult>
381403
{
382404
constructor(private readonly promise: Promise<ToolResponse<TResult>>) {}
@@ -386,8 +408,10 @@ export class ToolCallResponseReaderImpl<TResult>
386408
}
387409
}
388410

389-
export class ToolCallReaderImpl<TArgs, TResult>
390-
implements ToolCallReader<TArgs, TResult>
411+
export class ToolCallReaderImpl<
412+
TArgs extends ReadonlyJSONObject,
413+
TResult extends ReadonlyJSONValue,
414+
> implements ToolCallReader<TArgs, TResult>
391415
{
392416
public readonly args: ToolCallArgsReaderImpl<TArgs>;
393417
public readonly response: ToolCallResponseReaderImpl<TResult>;

packages/assistant-stream/src/core/tool/ToolExecutionStream.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
AssistantMetaTransformStream,
66
} from "../utils/stream/AssistantMetaTransformStream";
77
import { PipeableTransformStream } from "../utils/stream/PipeableTransformStream";
8-
import { ReadonlyJSONValue } from "../../utils/json/json-value";
8+
import {
9+
ReadonlyJSONObject,
10+
ReadonlyJSONValue,
11+
} from "../../utils/json/json-value";
912
import { ToolResponse } from "./ToolResponse";
1013
import { withPromiseOrValue } from "../utils/withPromiseOrValue";
1114
import { ToolCallReaderImpl } from "./ToolCallReader";
@@ -14,13 +17,16 @@ import { ToolCallReader } from "./tool-types";
1417
type ToolCallback = (toolCall: {
1518
toolCallId: string;
1619
toolName: string;
17-
args: unknown;
20+
args: ReadonlyJSONObject;
1821
}) =>
1922
| Promise<ToolResponse<ReadonlyJSONValue>>
2023
| ToolResponse<ReadonlyJSONValue>
2124
| undefined;
2225

23-
type ToolStreamCallback = <TArgs, TResult>(toolCall: {
26+
type ToolStreamCallback = <
27+
TArgs extends ReadonlyJSONObject = ReadonlyJSONObject,
28+
TResult extends ReadonlyJSONValue = ReadonlyJSONValue,
29+
>(toolCall: {
2430
reader: ToolCallReader<TArgs, TResult>;
2531
toolCallId: string;
2632
toolName: string;
@@ -39,7 +45,7 @@ export class ToolExecutionStream extends PipeableTransformStream<
3945
const toolCallPromises = new Map<string, PromiseLike<void>>();
4046
const toolCallControllers = new Map<
4147
string,
42-
ToolCallReaderImpl<unknown, unknown>
48+
ToolCallReaderImpl<ReadonlyJSONObject, ReadonlyJSONValue>
4349
>();
4450

4551
super((readable) => {
@@ -58,7 +64,10 @@ export class ToolExecutionStream extends PipeableTransformStream<
5864
switch (type) {
5965
case "part-start":
6066
if (chunk.part.type === "tool-call") {
61-
const reader = new ToolCallReaderImpl<unknown, unknown>();
67+
const reader = new ToolCallReaderImpl<
68+
ReadonlyJSONObject,
69+
ReadonlyJSONValue
70+
>();
6271
toolCallControllers.set(chunk.part.toolCallId, reader);
6372

6473
options.streamCall({

packages/assistant-stream/src/core/tool/ToolResponse.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export class ToolResponse<TResult> {
2323
this.isError = options.isError ?? false;
2424
}
2525

26-
static [Symbol.hasInstance](obj: unknown): obj is ToolResponse<unknown> {
26+
static [Symbol.hasInstance](
27+
obj: unknown,
28+
): obj is ToolResponse<ReadonlyJSONValue> {
2729
return (
2830
typeof obj === "object" && obj !== null && TOOL_RESPONSE_SYMBOL in obj
2931
);

packages/assistant-stream/src/core/tool/tool-types.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { JSONSchema7 } from "json-schema";
2-
import { TypeAtPath, TypePath } from "./type-path-utils";
3-
import { DeepPartial } from "ai";
2+
import { DeepPartial, TypeAtPath, TypePath } from "./type-path-utils";
43
import { AsyncIterableStream } from "../../utils";
54
import type { StandardSchemaV1 } from "@standard-schema/spec";
65
import { ToolResponse } from "./ToolResponse";
@@ -13,7 +12,7 @@ import { ToolResponse } from "./ToolResponse";
1312
*
1413
* @template TArgs The type of arguments being read.
1514
*/
16-
export interface ToolCallArgsReader<TArgs> {
15+
export interface ToolCallArgsReader<TArgs extends Record<string, unknown>> {
1716
/**
1817
* Returns a promise that will resolve to the value at the given path,
1918
* as soon as that path is generated by the LLM.
@@ -63,7 +62,10 @@ export interface ToolCallResponseReader<TResult> {
6362
get: () => Promise<ToolResponse<TResult>>;
6463
}
6564

66-
export interface ToolCallReader<TArgs, TResult> {
65+
export interface ToolCallReader<
66+
TArgs extends Record<string, unknown> = Record<string, unknown>,
67+
TResult = unknown,
68+
> {
6769
args: ToolCallArgsReader<TArgs>;
6870
response: ToolCallResponseReader<TResult>;
6971

@@ -85,7 +87,10 @@ export type ToolExecuteFunction<TArgs, TResult> = (
8587
context: ToolExecutionContext,
8688
) => TResult | Promise<TResult>;
8789

88-
export type ToolStreamCallFunction<TArgs, TResult> = (
90+
export type ToolStreamCallFunction<
91+
TArgs extends Record<string, unknown> = Record<string, unknown>,
92+
TResult = unknown,
93+
> = (
8994
reader: ToolCallReader<TArgs, TResult>,
9095
context: ToolExecutionContext,
9196
) => void;

0 commit comments

Comments
 (0)