Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/five-coins-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": patch
---

fix: allow callable methods to return this.state
16 changes: 15 additions & 1 deletion packages/agents/src/serializable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,26 @@ type AllSerializableValues<A> = A extends [infer First, ...infer Rest]
// biome-ignore lint: suspicious/noExplicitAny
export type Method = (...args: any[]) => any;

// Helper to check if a type is exactly unknown
// unknown extends T is true only if T is unknown or any
// We also need [T] extends [unknown] to handle distribution
type IsUnknown<T> = [unknown] extends [T]
? [T] extends [unknown]
? true
: false
: false;

// Helper to unwrap Promise and check if the inner type is unknown
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

export type RPCMethod<T = Method> = T extends Method
? T extends (...arg: infer A) => infer R
? AllSerializableValues<A> extends true
? R extends SerializableReturnValue
? T
: never
: IsUnknown<UnwrapPromise<R>> extends true
? T
: never
: never
: never
: never;
47 changes: 47 additions & 0 deletions packages/agents/src/tests-d/example-stub.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@ class MyAgent extends Agent<typeof env, {}> {
}
}

// Test case for issue #598: callable returning this.state
type MyState = { count: number; name: string };

class AgentWithState extends Agent<typeof env, MyState> {
// Explicit return type annotation - this should work
@callable()
async getInternalExplicit(): Promise<MyState> {
return this.state;
}

// No explicit return type - TypeScript infers the return type
// This is the case reported in issue #598
@callable()
async getInternal() {
return this.state;
}

@callable()
getInternalSync() {
return this.state;
}
}

// Test with default unknown state - this is the case reported in issue #598
class AgentWithUnknownState extends Agent<typeof env> {
@callable()
async getInternal() {
return this.state;
}
}

const { stub } = useAgent<MyAgent, {}>({ agent: "my-agent" });
// return type is promisified
stub.sayHello() satisfies Promise<string>;
Expand All @@ -45,3 +76,19 @@ const { stub: stub2 } = useAgent<Omit<MyAgent, "nonRpc">, {}>({
stub2.sayHello();
// @ts-expect-error nonRpc excluded from useAgent
stub2.nonRpc();

// Test case for https://github.com/cloudflare/agents/issues/598
const { stub: stubWithState } = useAgent<AgentWithState, MyState>({
agent: "agent-with-state"
});

// These should work without TypeScript errors
stubWithState.getInternalExplicit() satisfies Promise<MyState>;
stubWithState.getInternal() satisfies Promise<MyState>;
stubWithState.getInternalSync() satisfies Promise<MyState>;

// Test with unknown state
const { stub: stubUnknown } = useAgent<AgentWithUnknownState, unknown>({
agent: "agent-unknown"
});
stubUnknown.getInternal() satisfies Promise<unknown>;
Loading