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
2 changes: 1 addition & 1 deletion docs/api/primitive-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Primitive catalog — the never-stale anti-reinvention inventory

> **GENERATED** from `@tangle-network/agent-runtime@0.123.0` and `@tangle-network/agent-eval@0.142.2` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`.
> **GENERATED** from `@tangle-network/agent-runtime@0.123.1` and `@tangle-network/agent-eval@0.142.2` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`.

## 1. agent-runtime — own public surface

Expand Down
22 changes: 22 additions & 0 deletions docs/api/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -12274,6 +12274,28 @@ Generic environment provider executor config. External packages implement

[`ProviderExecutorOptions`](runtime/environment-provider.md#providerexecutoroptions).[`requireTerminalEvent`](runtime/environment-provider.md#requireterminalevent-1)

##### profileForCreate?

> `optional` **profileForCreate?**: (`profile`) => `AgentProfile`

Transform only the profile sent to `provider.create`. The original profile
remains the input to `taskToTurn`, so execution-only normalization cannot
rewrite the caller's task mapping.

###### Parameters

###### profile

`AgentProfile`

###### Returns

`AgentProfile`

###### Inherited from

[`ProviderExecutorOptions`](runtime/environment-provider.md#providerexecutoroptions).[`profileForCreate`](runtime/environment-provider.md#profileforcreate)

##### taskToTurn?

> `optional` **taskToTurn?**: (`task`, `specProfile`) => `AgentTurnInput`
Expand Down
20 changes: 20 additions & 0 deletions docs/api/runtime/environment-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,26 @@ Options for running a provider as a supervise-mode executor.

**`Experimental`**

##### profileForCreate?

> `optional` **profileForCreate?**: (`profile`) => `AgentProfile`

**`Experimental`**

Transform only the profile sent to `provider.create`. The original profile
remains the input to `taskToTurn`, so execution-only normalization cannot
rewrite the caller's task mapping.

###### Parameters

###### profile

`AgentProfile`

###### Returns

`AgentProfile`

##### taskToTurn?

> `optional` **taskToTurn?**: (`task`, `specProfile`) => `AgentTurnInput`
Expand Down
2 changes: 1 addition & 1 deletion docs/canonical-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Generated signatures and the complete export list live in docs/api/.
Run pnpm docs:freshness after editing this file. -->

> **Version 0.123.0.**
> **Version 0.123.1.**
> [`docs/api/primitive-catalog.md`](./api/primitive-catalog.md) lists every export and import path.
> `agent-eval` must satisfy `>=0.142.2 <0.143.0`.
> `sandbox` must satisfy `>=0.17.2 <0.18.0`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-runtime",
"version": "0.123.0",
"version": "0.123.1",
"description": "Shared task-lifecycle skeleton for agents: a recursive loop kernel for chat turns, one-shot tasks, and multi-attempt loops, with trace capture and eval-gated self-improvement. Domain behavior lives in adapters; scoring and ship-gates in @tangle-network/agent-eval.",
"homepage": "https://github.com/tangle-network/agent-runtime#readme",
"repository": {
Expand Down
5 changes: 3 additions & 2 deletions src/improvement/agentic-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
runLocalHarness,
} from '../mcp/local-harness'
import { runSettledCommand } from '../mcp/worktree-harness'
import { concreteProfileModel } from '../runtime/supervise/model-policy'
import type { CandidateGenerator } from './improvement-driver'
import { optimizerMethod } from './optimizer-prompt'

Expand Down Expand Up @@ -325,7 +326,7 @@ export function agenticGenerator(opts: AgenticGeneratorOptions = {}): CandidateG
}

if (reproducibleCostLedger) {
const model = opts.profile?.model?.default
const model = opts.profile ? concreteProfileModel(opts.profile) : undefined
if (!model) {
throw new Error('agenticGenerator: reproducible Codex requires profile.model.default')
}
Expand Down Expand Up @@ -702,7 +703,7 @@ function shotReceipt(input: {
shot: input.shot + 1,
maxShots: input.maxShots,
harness: input.harness,
model: input.profile?.model?.default ?? null,
model: input.profile ? (concreteProfileModel(input.profile) ?? null) : null,
reasoningEffort: input.profile?.model?.reasoningEffort ?? null,
promptSha256: sha256(input.prompt),
startedAt: input.startedAt.toISOString(),
Expand Down
9 changes: 5 additions & 4 deletions src/mcp/local-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { homedir, tmpdir } from 'node:os'
import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from 'node:path'
import type { AgentProfile, HarnessType, ReasoningEffort } from '@tangle-network/agent-interface'
import { concreteProfileModel } from '../runtime/supervise/model-policy'
import {
codexSensitiveEnvironmentName,
collectCodexDiagnosticRedactionValues,
Expand Down Expand Up @@ -308,8 +309,8 @@ export function harnessInvocation(
}

if (options.codexReproducible) {
const model = profile.model?.default
if (typeof model !== 'string' || model.trim().length === 0) {
const model = concreteProfileModel(profile)
if (!model) {
throw new Error('harnessInvocation: codexReproducible requires profile.model.default')
}
if (profile.model?.reasoningEffort === undefined) {
Expand All @@ -335,8 +336,8 @@ export function harnessInvocation(

const args = buildHarnessArgs(harness, composedPrompt, options)

const model = profile.model?.default
if (typeof model === 'string' && model.length > 0) {
const model = concreteProfileModel(profile)
if (model) {
args.push(...invocation.modelArgs(model))
}

Expand Down
54 changes: 53 additions & 1 deletion src/runtime/define-leaderboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { SandboxEvent } from '@tangle-network/sandbox'
import { HARNESS_NATIVE_MODEL } from '@tangle-network/agent-eval'
import type { AgentProfile } from '@tangle-network/agent-interface'
import type { CreateSandboxOptions, SandboxEvent } from '@tangle-network/sandbox'
import { describe, expect, it } from 'vitest'
import {
defineLeaderboard,
Expand Down Expand Up @@ -176,7 +178,20 @@ describe('defineLeaderboard', () => {
/paid-call receipt/,
)

const creates: CreateSandboxOptions[] = []
const result = await board({
backends: {
inproc: () => {
const inner = fakeBackend()
return {
...inner,
async create(options?: CreateSandboxOptions) {
creates.push(options ?? {})
return inner.create(options)
},
}
},
},
resolveModel: (events) => {
// The served model rides the backend's own usage events — here the fake
// backend's llm_call stands in for the harness's terminal event.
Expand All @@ -185,6 +200,43 @@ describe('defineLeaderboard', () => {
},
}).run([...snappedAxis, '--cases', 'case-alpha'])
expect(result.records[0]?.model).toBe('kimi-k2@2026-01-01')
expect(creates).toHaveLength(1)
expect(creates[0]?.backend?.model).toBeUndefined()
const executionProfile = creates[0]?.backend?.profile as AgentProfile
expect(executionProfile.model?.default).toBeUndefined()
expect(executionProfile.model?.default).not.toBe(HARNESS_NATIVE_MODEL)
})

it('sends the runtime-selected marker only to cli-bridge cells', async () => {
const creates: CreateSandboxOptions[] = []
const inner = fakeBackend()
const result = await board({
backends: {
'cli-bridge': () => ({
...inner,
async create(options?: CreateSandboxOptions) {
creates.push(options ?? {})
return inner.create(options)
},
}),
},
resolveModel: () => 'kimi-k2@2026-01-01',
}).run([
'--backend',
'cli-bridge',
'--harnesses',
'claude-code',
'--models',
'moonshot/kimi-k2@2026-01-01',
'--cases',
'case-alpha',
])

expect(result.records[0]?.model).toBe('kimi-k2@2026-01-01')
expect(creates).toHaveLength(1)
expect(creates[0]?.backend?.model?.model).toBe(HARNESS_NATIVE_MODEL)
const executionProfile = creates[0]?.backend?.profile as AgentProfile
expect(executionProfile.model?.default).toBeUndefined()
})

it('flows a structured TArtifact through parseOutput → score → records natively', async () => {
Expand Down
9 changes: 8 additions & 1 deletion src/runtime/define-leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { leaderboard, renderLeaderboardMarkdown } from './benchmark-report'
import { loopDispatch } from './loop-dispatch'
import { resolveSandboxClient } from './resolve-sandbox-client'
import { type SteeringDecision, steeringDriver } from './steering-drivers'
import { isHarnessNativeModel } from './supervise/model-policy'
import type { LoopResult, SandboxClient } from './types'

/** Structured per-case verdict a `score` function may return (a bare number is
Expand Down Expand Up @@ -513,6 +514,12 @@ export function defineLeaderboard<TCase, TArtifact = string>(
// whichever backend client runs the cell.
const axis = harnessAxisOf(cellProfile)
const modelId = bareModel(axis?.model ?? models[0] ?? '')
const backendModel = {
...spec.modelBackend,
...(!isHarnessNativeModel(modelId) || backendName === 'cli-bridge'
? { model: modelId }
: {}),
}
return {
// The naive steering directive = the no-signal retry floor: re-run the same case as
// an independent attempt until one scores (>0) or the shot cap. The policy is data
Expand All @@ -532,7 +539,7 @@ export function defineLeaderboard<TCase, TArtifact = string>(
sandboxOverrides: {
backend: {
type: axis.harness,
model: { ...spec.modelBackend, model: modelId },
...(Object.keys(backendModel).length > 0 ? { model: backendModel } : {}),
},
} as never,
}
Expand Down
48 changes: 48 additions & 0 deletions src/runtime/environment-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HARNESS_NATIVE_MODEL } from '@tangle-network/agent-eval'
import type { AgentProfile } from '@tangle-network/agent-interface'
import type {
BackendType,
Expand Down Expand Up @@ -1085,6 +1086,53 @@ describe('environment provider adapters', () => {
expect(executor.resultArtifact().out).toMatchObject({ content: 'from-package' })
})

it('keeps the runtime-selected model marker out of provider.create', async () => {
let createdProfile: AgentProfile | string | undefined
let taskProfile: AgentProfile | undefined
const provider: AgentEnvironmentProvider = {
name: 'runtime-model-provider',
capabilities: () => fakeCapabilities(),
async create(input) {
createdProfile = input.profile
return fakeEnvironment({
stream: async function* (): AsyncIterable<AgentEnvironmentEvent> {
yield { type: 'result', data: { finalText: 'provider-selected-model' } }
},
})
},
}
const factory = createExecutor({
backend: 'provider',
provider,
profileForCreate: (profile) => ({ ...profile, description: 'create-only transform' }),
taskToTurn: (task, profile) => {
taskProfile = profile
return { prompt: String(task) }
},
})
const profile: AgentProfile = {
name: 'runtime-model-worker',
model: {
provider: 'tangle-router',
default: ` ${HARNESS_NATIVE_MODEL} `,
reasoningEffort: 'high',
},
}
const spec: AgentSpec = { profile, harness: null }
const ctx: ExecutorContext = { signal: new AbortController().signal, seams: {} }
const executor = factory(spec, ctx)

await collect(executor.execute('task', ctx.signal) as AsyncIterable<UsageEvent>)

expect(createdProfile).toMatchObject({
name: 'runtime-model-worker',
description: 'create-only transform',
model: { provider: 'tangle-router', reasoningEffort: 'high' },
})
expect((createdProfile as AgentProfile).model?.default).toBeUndefined()
expect(taskProfile).toBe(profile)
})

it('resolves a named provider through the runtime registry', async () => {
let created: unknown
const provider: AgentEnvironmentProvider = {
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/environment-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ export interface ProviderExecutorOptions {
runtime?: Runtime
destroyOnSettle?: boolean
requireTerminalEvent?: boolean
/** Transform only the profile sent to `provider.create`. The original profile
* remains the input to `taskToTurn`, so execution-only normalization cannot
* rewrite the caller's task mapping. */
profileForCreate?: (profile: AgentProfile) => AgentProfile
taskToTurn?: (task: unknown, specProfile: AgentProfile) => AgentTurnInput
}

Expand Down Expand Up @@ -387,9 +391,10 @@ async function* streamProviderExecutor(
): AsyncIterable<UsageEvent> {
const started = Date.now()
const linked = mergeAbortSignals(args.signal, args.controller.signal)
const createProfile = args.options.profileForCreate?.(args.profile) ?? args.profile
const environment = await args.provider.create({
...(args.options.defaults ?? {}),
profile: args.profile,
profile: createProfile,
signal: linked,
})
args.onEnvironment(environment)
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/sandbox-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import type { AgentProfile, HarnessType } from '@tangle-network/agent-interface'
import type { CreateSandboxOptions } from '@tangle-network/sandbox'
import { profileForExecution } from './supervise/model-policy'

type BackendType = NonNullable<CreateSandboxOptions['backend']>['type']
type BackendOverride = NonNullable<CreateSandboxOptions['backend']>
Expand Down Expand Up @@ -88,7 +89,7 @@ export function buildBackendOptions(
...base,
backend: {
type: resolveBackendType(profile, overrideBackend),
profile,
profile: profileForExecution(profile),
...(overrideBackend?.model ? { model: overrideBackend.model } : {}),
...(overrideBackend?.server ? { server: overrideBackend.server } : {}),
},
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/supervise/authoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ValidationError } from '../../errors'
import { type RouterConfig, routerChatWithUsage } from '../router-client'
import { type DeliverableSpec, gateOnDeliverable } from './completion-gate'
import { attestRuntimeOwnedExecutor, newExecutionAttemptId } from './materialization'
import { concreteProfileModel } from './model-policy'
import { supervisorPolicyPrompt } from './prompt-registry'
import type { Agent, AgentSpec, Executor, ExecutorResult } from './types'

Expand Down Expand Up @@ -138,7 +139,7 @@ export function authoredWorker(
temperature?: number
},
): Agent<unknown, unknown> {
const model = profile.model?.default ?? opts.cfg.model
const model = concreteProfileModel(profile) ?? opts.cfg.model
const executorFactory: NonNullable<AgentSpec['executorFactory']> = (spec, ctx) => {
let artifact: ExecutorResult<unknown> | undefined
const executionId = ctx.node?.nodeId ?? `authored-router-${profile.name}`
Expand Down
Loading