Skip to content

Commit 3d661c2

Browse files
CCR 3
Type the handle's metadata as deeply readonly. `defineFactory` deep-freezes the snapshot it stores, but `FactoryHandle.meta` was typed as a mutable `FactoryMeta`, so `handle.meta.name = "..."` and `handle.meta.phases.push(...)` compiled and then threw at runtime. The test asserts both halves: the mutation is a type error and it also throws.
1 parent 168a6a8 commit 3d661c2

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

nodejs/src/factory.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,24 @@ export interface FactoryDefinition<
194194
* @experimental Part of the experimental Agent Factories surface and may
195195
* change or be removed in future SDK or CLI releases.
196196
*/
197+
/**
198+
* A deeply immutable view of a value.
199+
*
200+
* `defineFactory` deep-freezes the metadata it stores, so the handle's view of
201+
* it has to be readonly all the way down or `handle.meta.name = "..."` and
202+
* `handle.meta.phases.push(...)` would compile and then throw at runtime.
203+
*/
204+
type DeepReadonly<T> = T extends (infer U)[]
205+
? readonly DeepReadonly<U>[]
206+
: T extends object
207+
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
208+
: T;
209+
197210
export interface FactoryHandle<
198211
TArgs extends JsonValue = JsonValue,
199212
TResult extends JsonValue | void = JsonValue | void,
200213
> {
201-
readonly meta: FactoryMeta;
214+
readonly meta: DeepReadonly<FactoryMeta>;
202215
readonly [factoryHandleBrand]: {
203216
readonly args: TArgs;
204217
readonly result: TResult;
@@ -422,7 +435,7 @@ export function defineFactory<
422435
meta,
423436
run: definition.run,
424437
};
425-
const handle = Object.freeze({ meta }) as FactoryHandle<TArgs, TResult>;
438+
const handle = Object.freeze({ meta }) as unknown as FactoryHandle<TArgs, TResult>;
426439

427440
factoryHandles.set(handle, stored);
428441
return handle;

nodejs/test/factory.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ describe("factories", () => {
6969
meta.name = "no-limits";
7070
meta.phases.length = 0;
7171

72+
// The stored metadata is deep-frozen, so the handle's view of it must be
73+
// readonly all the way down. Assert both halves: the mutation is a type
74+
// error, and it also throws at runtime.
75+
expect(() => {
76+
// @ts-expect-error handle.meta is deeply readonly.
77+
handle.meta.name = "mutated";
78+
}).toThrow(TypeError);
79+
expect(() => {
80+
// @ts-expect-error handle.meta.phases is a readonly array.
81+
handle.meta.phases.push({ title: "late" });
82+
}).toThrow(TypeError);
83+
7284
const session = new CopilotSession("session-1", {} as never);
7385
session.registerFactories([handle]);
7486
const result = await session.clientSessionApis.factory!.execute({

0 commit comments

Comments
 (0)