Skip to content
Open
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/harness-agents-of-ts71.md
Original file line number Diff line number Diff line change
@@ -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<SessionImpl<…>, 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.
29 changes: 18 additions & 11 deletions packages/ai-harness/src/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,18 +482,25 @@ export type SessionMetadataOf<THarness> =
? MetadataOf<TMetadataSchema>
: Record<string, unknown>

/** 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> =
THarness extends HarnessDefinition<
any,
infer TAgents,
infer TSubagents,
any,
ReadonlyArray<AnyTool>,
any
>
? TAgents[number] | TSubagents[number]
: never
| (THarness extends {
readonly agents?: infer TAgents extends ReadonlyArray<AnyAgent>
}
? TAgents[number]
: never)
| (THarness extends {
readonly subagents?: {
agents: infer TSubagents extends ReadonlyArray<AnyAgent>
}
}
? TSubagents[number]
: never)

/** The `tools` of a harness, as a union. */
export type HarnessToolsOf<THarness> =
Expand Down
22 changes: 22 additions & 0 deletions packages/ai-harness/tests/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
import type { SessionIndexEntry } from '@tanstack/ai-persistence'
import type {
AgentActions,
AnyHarness,
AgentRun,
AgentSettled,
HarnessAgentContext,
Expand All @@ -32,6 +33,7 @@ import type {
HarnessSession,
Operation,
PluginLifetime,
HarnessAgentsOf,
HarnessUser,
ProjectOptions,
Receipt,
Expand Down Expand Up @@ -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<HarnessAgentsOf<typeof harness>>().toEqualTypeOf<
typeof pricer | typeof writer
>()
const agentsOnly = defineHarness({
name: 'test/agents-only',
adapter: mockAdapter([]).adapter,
agents: [pricer],
})
expectTypeOf<HarnessAgentsOf<typeof agentsOnly>>().toEqualTypeOf<
typeof pricer
>()
const noAgents = defineHarness({
name: 'test/no-agents',
adapter: mockAdapter([]).adapter,
})
expectTypeOf<HarnessAgentsOf<typeof noAgents>>().toBeNever()
expectTypeOf<HarnessAgentsOf<AnyHarness>>().toBeAny()
})

it('takes an inputId on every input and keeps the op narrowing', () => {
const inputs = [
{ op: 'prompt', message: 'hi', inputId: 'a' },
Expand Down
Loading