From f1266ad82faa6e155861a28d9a950af565e68547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Mon, 26 Oct 2026 14:14:28 +0100 Subject: [PATCH] fix(ai-harness): infer HarnessAgentsOf from properties to avoid a TS 7.1 circularity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under TypeScript 7.1, defineHarness() and host.open() fail with TS2615 ("Type of property 'agents' circularly references itself in mapped type 'Pick, PublicSessionMember>'"). HarnessAgentsOf matched the whole HarnessDefinition, whose session reaches AgentHandles, which remaps keys through HarnessAgentsOf again. Read agents and subagents.agents alone. Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/harness-agents-of-ts71.md | 5 ++++ packages/ai-harness/src/define.ts | 29 ++++++++++++++--------- packages/ai-harness/tests/types.test-d.ts | 22 +++++++++++++++++ 3 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 .changeset/harness-agents-of-ts71.md diff --git a/.changeset/harness-agents-of-ts71.md b/.changeset/harness-agents-of-ts71.md new file mode 100644 index 0000000000..6c31297c1f --- /dev/null +++ b/.changeset/harness-agents-of-ts71.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-harness': patch +--- + +Fix TS2615 under TypeScript 7.1: "Type of property 'agents' circularly references itself in mapped type 'Pick, PublicSessionMember>'". `defineHarness()` and `host.open()` failed with it in code that TypeScript 7.0 accepts. `HarnessAgentsOf` now reads the `agents` and `subagents.agents` of a harness instead of matching the whole definition, which reached `session.agents` again. The types it gives are unchanged. diff --git a/packages/ai-harness/src/define.ts b/packages/ai-harness/src/define.ts index 47dc24d59d..197e688463 100644 --- a/packages/ai-harness/src/define.ts +++ b/packages/ai-harness/src/define.ts @@ -482,18 +482,25 @@ export type SessionMetadataOf = ? MetadataOf : Record -/** Every agent a harness registers, as a union. */ +/** + * Every agent a harness registers, as a union. It reads `agents` and + * `subagents.agents` alone: matching the whole definition reaches + * `session.agents`, which is typed by this, and TypeScript 7.1 reports the + * loop as a circular mapped type (TS2615). + */ export type HarnessAgentsOf = - THarness extends HarnessDefinition< - any, - infer TAgents, - infer TSubagents, - any, - ReadonlyArray, - any - > - ? TAgents[number] | TSubagents[number] - : never + | (THarness extends { + readonly agents?: infer TAgents extends ReadonlyArray + } + ? TAgents[number] + : never) + | (THarness extends { + readonly subagents?: { + agents: infer TSubagents extends ReadonlyArray + } + } + ? TSubagents[number] + : never) /** The `tools` of a harness, as a union. */ export type HarnessToolsOf = diff --git a/packages/ai-harness/tests/types.test-d.ts b/packages/ai-harness/tests/types.test-d.ts index d939525f29..8b725419a4 100644 --- a/packages/ai-harness/tests/types.test-d.ts +++ b/packages/ai-harness/tests/types.test-d.ts @@ -20,6 +20,7 @@ import type { import type { SessionIndexEntry } from '@tanstack/ai-persistence' import type { AgentActions, + AnyHarness, AgentRun, AgentSettled, HarnessAgentContext, @@ -32,6 +33,7 @@ import type { HarnessSession, Operation, PluginLifetime, + HarnessAgentsOf, HarnessUser, ProjectOptions, Receipt, @@ -82,6 +84,26 @@ it('types agent input and result from the definition', () => { void session.agents.pricer.run({ vendor: 1 }) }) +it('collects the agents of a harness from agents and subagents', () => { + expectTypeOf>().toEqualTypeOf< + typeof pricer | typeof writer + >() + const agentsOnly = defineHarness({ + name: 'test/agents-only', + adapter: mockAdapter([]).adapter, + agents: [pricer], + }) + expectTypeOf>().toEqualTypeOf< + typeof pricer + >() + const noAgents = defineHarness({ + name: 'test/no-agents', + adapter: mockAdapter([]).adapter, + }) + expectTypeOf>().toBeNever() + expectTypeOf>().toBeAny() +}) + it('takes an inputId on every input and keeps the op narrowing', () => { const inputs = [ { op: 'prompt', message: 'hi', inputId: 'a' },