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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: tangle-network/agent-eval
ref: 76969e551d54c7898f11253175e0da9652e0c4a8 # @tangle-network/agent-eval@0.146.0
ref: e07a6a4772424107e9831bd9e05512e52134e33b # @tangle-network/agent-eval@0.147.0
path: .cohort/agent-eval
persist-credentials: false

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: tangle-network/agent-eval
ref: 76969e551d54c7898f11253175e0da9652e0c4a8 # @tangle-network/agent-eval@0.146.0
ref: e07a6a4772424107e9831bd9e05512e52134e33b # @tangle-network/agent-eval@0.147.0
path: .cohort/agent-eval
persist-credentials: false

Expand Down
6 changes: 3 additions & 3 deletions bench/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-bench",
"version": "0.8.13",
"version": "0.8.14",
"type": "module",
"description": "Benchmark adapters and execution for agent-runtime across coding, tool-use, RAG, memory, browser, and terminal tasks.",
"repository": {
Expand Down Expand Up @@ -37,15 +37,15 @@
"typecheck:public": "tsc -p tsconfig.public.json",
"verify:package": "pnpm run verify:package:static && node scripts/verify-packed-consumer.mjs",
"verify:package:local-runtime": "pnpm run verify:package:static && node scripts/verify-packed-consumer.mjs --local-runtime",
"verify:package:static": "pnpm run build && publint && attw --pack --profile esm-only .",
"verify:package:static": "pnpm run build && publint && attw --pack --profile esm-only . && node ../scripts/check-published-ranges.mjs bench",
"wait:published-dependencies": "node scripts/wait-for-published-dependencies.mjs",
"verify:pier": "tsx scripts/verify-pier-pair.mts"
},
"dependencies": {
"@tangle-network/agent-eval": "catalog:",
"@tangle-network/agent-interface": "catalog:",
"@tangle-network/agent-knowledge": "catalog:",
"@tangle-network/agent-runtime": "workspace:*",
"@tangle-network/agent-runtime": "workspace:^",
"@tangle-network/sandbox": "catalog:"
},
"devDependencies": {
Expand Down
16 changes: 16 additions & 0 deletions docs/STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,19 @@ Promotion is a normal PR: flip the tags (per-symbol and module-level), add the C

A `@stable` symbol is demoted back to `@experimental`, or removed, only through a deprecation cycle: the release that announces it adds `@deprecated` (naming the replacement or the reason) while the symbol keeps working, the CHANGELOG entry names the symbol and the migration path, and removal lands no earlier than the next minor release after the announcement.
An `@experimental` symbol needs none of that — it can be reshaped or removed in any release with a CHANGELOG line — which is exactly why the default is experimental and the stable set is enumerated, not implied.

## What this package declares about its first-party dependencies

Every `@tangle-network/*` specifier this package publishes is a **range**, never one exact version.
An exact pin is not a compatibility statement.
It names one version and refuses every other, so a consumer that already holds a later cohort member installs a **second physical copy** of the pinned package.
Two copies of `@tangle-network/agent-interface` in one tree means two class identities and `instanceof` answering false across the seam.

The range shape follows the depended-on package's own versioning, the same rule a peer range follows:

- **From 1.0.0** — a caret, such as `^1.0.0`. A minor is additive there, so one copy holds across later minors.
- **Below 1.0.0** — the narrower window `>=X.Y.Z <X.Y+1.0`. A pre-1.0 minor may remove, so the range stops at the next minor.

The range is stated once, in the `catalog:` block of [`pnpm-workspace.yaml`](../pnpm-workspace.yaml).
A `catalog:` specifier and a `workspace:*` specifier are both replaced by an exact version when the package is packed, so a source manifest can look clean while only the packed manifest carries the defect.
`pnpm run check:published-ranges` therefore packs each publishable workspace package and reads the archive, and fails when a packed first-party specifier names one version instead of a range.
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.139.0` and `@tangle-network/agent-eval@0.146.0` 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.140.0` and `@tangle-network/agent-eval@0.147.0` 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
4 changes: 2 additions & 2 deletions docs/canonical-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
Generated signatures and the complete export list live in docs/api/.
Run pnpm docs:freshness after editing this file. -->

> **Version 0.139.0.**
> **Version 0.140.0.**
> [`docs/api/primitive-catalog.md`](./api/primitive-catalog.md) lists every export and import path.
> `agent-eval` must satisfy `>=0.146.0 <0.147.0`.
> `agent-eval` must satisfy `>=0.147.0 <0.148.0`.
> `sandbox` must satisfy `>=0.27.1 <0.28.0`.
> Portable profile and tool-part types come from `@tangle-network/agent-interface` `^1.0.0`.
>
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-runtime",
"version": "0.139.0",
"version": "0.140.0",
"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 Expand Up @@ -170,7 +170,7 @@
"license": "MIT",
"packageManager": "pnpm@11.17.0",
"peerDependencies": {
"@tangle-network/agent-eval": ">=0.146.0 <0.147.0",
"@tangle-network/agent-eval": ">=0.147.0 <0.148.0",
"@tangle-network/agent-interface": "^1.0.0",
"@tangle-network/sandbox": ">=0.27.1 <0.28.0"
},
Expand Down
30 changes: 15 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ allowBuilds:
catalog:
'@arethetypeswrong/cli': 0.18.5
'@modelcontextprotocol/sdk': 1.30.0
'@tangle-network/agent-core': 0.9.4
'@tangle-network/agent-core': ^0.9.4
'@types/node': 26.1.1
'@tangle-network/agent-eval': 0.146.0
'@tangle-network/agent-eval': 0.147.0
'@tangle-network/agent-interface': 1.0.0
'@tangle-network/agent-knowledge': 8.0.6
'@tangle-network/agent-profile-materialize': 0.16.0
'@tangle-network/agent-knowledge': ^8.0.6
'@tangle-network/agent-profile-materialize': ^0.16.0
'@tangle-network/agent-trace-contract': ^1.0.2
'@tangle-network/sandbox': 0.27.1
publint: 0.3.22
Expand Down
106 changes: 106 additions & 0 deletions scripts/check-published-ranges.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env node
/**
* Fail a workspace package that would publish an exact first-party version pin.
*
* An exact pin is not a compatibility statement. It names one version and
* refuses every other, so a consumer that already holds a later cohort member
* installs a SECOND physical copy of the pinned package. Two copies of
* `@tangle-network/agent-interface` in one tree means two class identities, two
* module registries, and `instanceof` answering false across the seam.
*
* The pin is rarely written by hand. A `catalog:` entry and a `workspace:*`
* specifier are both replaced by an exact version when the package is packed,
* so the source manifest looks clean and only the PACKED manifest carries the
* defect. This check therefore packs and reads the archive, never the source.
*
* Usage: node scripts/check-published-ranges.mjs [package-directory ...]
* Defaults to every publishable package in the workspace.
*/
import { execFileSync } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse as parseYaml } from 'yaml'
import {
assertFirstPartyRangeSpecs,
assertPublishableDependencySpecs,
} from './lib/packed-package-test.mjs'

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')

function workspacePackageDirectories() {
const workspacePath = join(repoRoot, 'pnpm-workspace.yaml')
const directories = [repoRoot]
if (!existsSync(workspacePath)) return directories
const workspace = parseYaml(readFileSync(workspacePath, 'utf8'))
for (const entry of workspace?.packages ?? []) {
if (typeof entry !== 'string' || entry.includes('*')) {
throw new Error(`pnpm-workspace.yaml entry is not a plain directory: ${String(entry)}`)
}
directories.push(resolve(repoRoot, entry))
}
return directories
}

function packedManifest(packageDirectory) {
const scratch = mkdtempSync(join(tmpdir(), 'agent-runtime-published-ranges-'))
try {
execFileSync('pnpm', ['pack', '--pack-destination', scratch], {
cwd: packageDirectory,
env: { ...process.env, npm_config_ignore_scripts: 'true' },
stdio: ['ignore', 'pipe', 'pipe'],
maxBuffer: 10 * 1024 * 1024,
})
const archives = readdirSync(scratch).filter((name) => name.endsWith('.tgz'))
if (archives.length !== 1) {
throw new Error(
`${packageDirectory} produced ${archives.length} archives, expected exactly one`,
)
}
return JSON.parse(
execFileSync('tar', ['-xOzf', join(scratch, archives[0]), 'package/package.json'], {
encoding: 'utf8',
maxBuffer: 4 * 1024 * 1024,
}),
)
} finally {
rmSync(scratch, { recursive: true, force: true })
}
}

const requested = process.argv.slice(2).map((entry) => resolve(repoRoot, entry))
const directories = requested.length > 0 ? requested : workspacePackageDirectories()
const failures = []

for (const directory of directories) {
const manifestPath = join(directory, 'package.json')
if (!existsSync(manifestPath)) throw new Error(`no package.json at ${directory}`)
const source = JSON.parse(readFileSync(manifestPath, 'utf8'))
if (source.private === true) continue
const manifest = packedManifest(directory)
try {
assertPublishableDependencySpecs(manifest)
assertFirstPartyRangeSpecs(manifest)
} catch (error) {
failures.push(error instanceof Error ? error.message : String(error))
continue
}
const firstParty = Object.entries({
...(manifest.dependencies ?? {}),
...(manifest.peerDependencies ?? {}),
})
.filter(([name]) => name.startsWith('@tangle-network/'))
.map(([name, spec]) => `${name}@${spec}`)
.sort()
process.stdout.write(
`${manifest.name}@${manifest.version} packed from ${relative(repoRoot, directory) || '.'}: ${
firstParty.length > 0 ? firstParty.join(', ') : 'no first-party dependencies'
}\n`,
)
}

if (failures.length > 0) {
process.stderr.write(`${failures.join('\n\n')}\n`)
process.exitCode = 1
}
65 changes: 64 additions & 1 deletion scripts/lib/packed-package-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,72 @@ export function caretAdmits(range, version) {
return minor * 1_000_000 + patch >= floorMinor * 1_000_000 + floorPatch
}

const exactVersion = /^\d+\.\d+\.\d+(?:[-+].*)?$/

/** True when a specifier names one version and admits no other. */
export function isExactVersionSpec(spec) {
return typeof spec === 'string' && exactVersion.test(spec.trim())
}

/**
* The cohort range a specifier states.
*
* A range states itself. An exact version states no range at all, so it is read
* as the range its own versioning earns — the shape `expectedPeerRange` returns.
*/
export function cohortRange(spec) {
if (typeof spec !== 'string' || spec.length === 0) {
throw new Error(`cannot read a cohort range from ${String(spec)}`)
}
return isExactVersionSpec(spec) ? expectedPeerRange(spec.trim()) : spec
}

/** A `>=floor <ceiling` window admits a version at or above the floor and below the ceiling. */
export function windowAdmits(range, version) {
const window = /^>=(\d+)\.(\d+)\.(\d+)\s+<(\d+)\.(\d+)\.(\d+)$/.exec(range)
const found = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
if (window === null || found === null) return false
const parts = window.slice(1).map(Number)
const order = ([major, minor, patch]) =>
major * 1_000_000_000_000 + minor * 1_000_000 + patch
const target = order(found.slice(1).map(Number))
return target >= order(parts.slice(0, 3)) && target < order(parts.slice(3))
}

/** A range admits a version through either supported cohort shape. */
export function rangeAdmits(range, version) {
return caretAdmits(range, version) || windowAdmits(range, version)
}

/**
* The published first-party specifiers a consumer resolves against.
*
* An exact version here forces a second physical copy of the named package for
* every consumer that already holds a later one, so every first-party
* specifier a consumer can read must be a range.
*/
export function assertFirstPartyRangeSpecs(packageJson, scope = '@tangle-network/') {
const packageName =
typeof packageJson.name === 'string' ? packageJson.name : 'packed package'
const exact = []
for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
for (const [name, spec] of Object.entries(packageJson[section] ?? {})) {
if (!name.startsWith(scope)) continue
if (isExactVersionSpec(spec)) exact.push(`${section}.${name} = ${spec}`)
}
}
if (exact.length > 0) {
throw new Error(
`${packageName} publishes exact first-party version pins, which duplicate the package for every consumer already holding a later one:\n${exact
.map((entry) => ` ${entry}`)
.join('\n')}\nDeclare a range instead: a caret from 1.0.0, or ">=X.Y.Z <X.Y+1.0" below it.`,
)
}
}

export function assertPeerMatchesDevelopmentDependency(packageJson, name) {
const version = requiredPackedDevelopmentDependency(packageJson, name)
const expected = expectedPeerRange(version)
const expected = cohortRange(version)
const actual = packageJson.peerDependencies?.[name]
if (actual !== expected) {
const packageName =
Expand Down
Loading
Loading