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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ dump.rdb
# Node.js
node_modules/
npm-debug.log*

# Local npm pack artifacts
clients/cli/*.tgz
yarn-debug.log*
yarn-error.log*

Expand Down
17 changes: 17 additions & 0 deletions clients/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to `@acnlabs/acn-cli` are documented here.

## [Unreleased]

## [1.0.40] - 2026-10-30

### Changed

- Quote / pricing docs and listen logs spell out **agent-owned** prices: you
fetch from your provider and report Host market currency; Host does not
maintain per-agent books. ``quote_hook_missing`` / ``quote_unpriced`` log
lines point authors at ``--on-quote-model-prices`` /
``ACN_ON_QUOTE_MODEL_PRICES`` / ``ACN_MODEL_PRICES`` / heartbeat
``model_prices``. Comiclaw scripts remain an example only.
- Comiclaw quote example: local TokenPlan stickers stay CNY; hook converts to
Host USD via ``COMICLAW_CNY_PER_USD`` (default 7.2) before answering. Local
miss → provider catalog/page lookup and cache upsert; optional
``COMICLAW_MODEL_PRICES_MAX_AGE_HOURS`` treats an aged local file as empty.
- Docs: price order Host book → valid agent local → provider lookup/self-quote
→ omit; agents refresh local rates on their own schedule.

## [1.0.39] - 2026-10-30

### Added
Expand Down
10 changes: 10 additions & 0 deletions clients/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ acn listen --exec './handle-a2a.sh' # stdout = full A2A JSON-RPC body
Keep `acn listen` and `acn heartbeat` in the same lifecycle for idle agents
(see [listen + heartbeat systemd example](../../docs/runbooks/acn-listen-heartbeat.md)).

**Model prices (agent-owned):** menus need a price. Order: **Host book** → your
**valid local / heartbeat `model_prices`** → only then **look up your provider**
(or self-quote) via catalog / `--on-quote-model-prices` /
`ACN_ON_QUOTE_MODEL_PRICES` → still missing → **omit** (Host does not maintain
your book). Refresh local rates on your own schedule; stale = missing. Quote
hook stdin `{model_ids,retry}` → stdout `{model_prices:[...]}` in **Host market
currency** (AgentPlanet: USD / million, no currency field). Optional:
`ACN_MODEL_PRICES`. See the runbook; `scripts/comiclaw/quote-model-prices.sh`
is one-agent example only.

### `acn agents`

Discover agents on ACN.
Expand Down
4 changes: 2 additions & 2 deletions clients/cli/package-lock.json

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

2 changes: 1 addition & 1 deletion clients/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@acnlabs/acn-cli",
"version": "1.0.39",
"version": "1.0.40",
"description": "Official CLI for ACN (Agent Collaboration Network) — zero-integration agent access",
"main": "dist/index.js",
"bin": {
Expand Down
22 changes: 18 additions & 4 deletions clients/cli/src/commands/listen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ export interface HandlerOptions {
supportedModels?: string[];
onSetPreferredModel?: string;
/**
* Shell command for Host price_required / quote_model_prices.
* Agent-owned hook: Host price_required / quote_model_prices.
* Fetch from your provider; report Host market currency / million.
* stdin JSON { model_ids, retry } → stdout JSON { model_prices: [...] }.
* Env: ACN_ON_QUOTE_MODEL_PRICES.
*/
Expand Down Expand Up @@ -265,10 +266,14 @@ async function handleQuoteModelPrices(
`quote_hook ${hooked.prices.length}${ask.retry ? ' retry' : ''}`
);
} else if (!hooked.ok) {
deps.logFn?.(`quote_hook_failed ${hooked.reason}`);
deps.logFn?.(
`quote_hook_failed ${hooked.reason} — agent must fetch provider prices and report Host market currency`
);
}
} else if (needHook && !hookCmd) {
deps.logFn?.('quote_hook_missing');
deps.logFn?.(
'quote_hook_missing — set --on-quote-model-prices / ACN_ON_QUOTE_MODEL_PRICES; agent fetches provider prices and reports (Host market currency); Host does not maintain your book'
);
}
// Env fallback for ids still missing after catalog + hook.
const stillMissing = ask.modelIds.filter(
Expand All @@ -287,6 +292,15 @@ async function handleQuoteModelPrices(
}
}
}
const unpriced = ask.modelIds.filter(
(id) =>
!prices.some((row) => row.model_id.trim().toLowerCase() === id.trim().toLowerCase())
);
if (unpriced.length) {
deps.logFn?.(
`quote_unpriced ${unpriced.join(',')} — menus omit unpriced models; fetch from your provider, save if needed, report via hook / ACN_MODEL_PRICES / heartbeat model_prices`
);
}
if (ask.retry && !prices.length) {
deps.logFn?.(`price_required ${ask.modelIds.join(',')}`);
}
Expand Down Expand Up @@ -1100,7 +1114,7 @@ export function listenCommand(): Command {
)
.option(
'--on-quote-model-prices <cmd>',
'When Host asks for prices the book missed: stdin JSON {model_ids,retry} → stdout JSON {model_prices:[...]} (env: ACN_ON_QUOTE_MODEL_PRICES)'
'Agent-owned: when Host asks for prices it cannot price, run cmd (stdin {model_ids,retry} → stdout {model_prices:[...]} in Host market currency). You fetch from your provider; Host does not maintain your book (env: ACN_ON_QUOTE_MODEL_PRICES)'
)
.action(
async (opts: {
Expand Down
42 changes: 42 additions & 0 deletions clients/cli/tests/listen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,48 @@ describe('preferred-model apply intercept', () => {
else process.env.ACN_PROVIDER_MODELS_URL = prevUrl;
});

it('logs quote_hook_missing and quote_unpriced when agent has no price source', async () => {
const prev = process.env.ACN_MODEL_PRICES;
delete process.env.ACN_MODEL_PRICES;
const prevUrl = process.env.ACN_PROVIDER_MODELS_URL;
delete process.env.ACN_PROVIDER_MODELS_URL;
const logs: string[] = [];
const fetchFn = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ status: 'ok' }),
} as unknown as Response);
const frames: OutboundFrame[] = [];
await dispatchA2aRequest(
ownerApplyRequest({
path: '/acn/v1/runtime',
body: JSON.stringify({
quote_model_prices: ['tencenttokenplan/no-such-model'],
error: 'price_required',
}),
headers: {
'content-type': 'application/json',
'x-acn-runtime-apply': '1',
},
}),
{
forward: 'http://localhost:8080',
agentId: 'ag1',
apiKey: 'k',
baseUrl: 'https://api.acnlabs.dev',
},
(f) => frames.push(f),
{ fetchFn, logFn: (line) => logs.push(line) }
);
expect(frames[0]).toMatchObject({ type: 'a2a_response', status: 200 });
expect(logs.some((l) => l.startsWith('quote_hook_missing'))).toBe(true);
expect(logs.some((l) => l.startsWith('quote_unpriced'))).toBe(true);
if (prev === undefined) delete process.env.ACN_MODEL_PRICES;
else process.env.ACN_MODEL_PRICES = prev;
if (prevUrl === undefined) delete process.env.ACN_PROVIDER_MODELS_URL;
else process.env.ACN_PROVIDER_MODELS_URL = prevUrl;
});

it('heartbeats the current model list when asked to report again', async () => {
const prev = process.env.ACN_SUPPORTED_MODELS;
const prevHub = process.env.ACN_PROVIDER_API_KEY;
Expand Down
38 changes: 28 additions & 10 deletions docs/runbooks/acn-listen-heartbeat.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,35 @@ sudo systemctl enable --now acn-heartbeat.timer
provider that owns the model.
- Matching **prices** on list rows are heartbeated as `model_prices`. Pricing
does not shrink the declaration. Interfaze Composer / Owner Settings show a
model only when Host can **price** it (official book → agent `model_prices`
→ agent quote). Unpriceable ids stay on `providers` but are **not** listed
in menus. If the current provider has no priced model, Composer tries the
agent's other declared providers, then the official list. Settings lists
each declared provider; Official · OpenRouter stays a platform row and is
not inferred from model-id prefixes.
- Host `model-price-quote` / `price_required` is answered by `acn listen`:
live provider catalog fields → **`--on-quote-model-prices` /
model only when Host can **price** it. Unpriceable ids stay on `providers`
but are **not** listed in menus. If the current provider has no priced model,
Composer tries the agent's other declared providers, then the official list.
Settings lists each declared provider; Official · OpenRouter stays a platform
row and is not inferred from model-id prefixes.
- **Price resolution (Host + agent):**
1. **Host book** (e.g. OpenRouter) — if Host already has a rate, use it; do
not ask the agent.
2. Else **agent local / already-reported** `model_prices` that are still
**valid** (agent-chosen TTL / refresh).
3. Else agent **looks up its provider** (API, docs, or site) or self-quotes,
then reports (heartbeat and/or quote hook). Rates must be **Host market
currency** / million (no currency field — AgentPlanet: USD).
4. Still nothing → **omit from menus** (no “not priced” row).
- **Who owns prices:** the **agent** (runtime / provider integration), not the
Host operator. Host will not maintain per-agent books. Agents should
**refresh local rates periodically**; stale local rows count as missing and
fall through to step 3.
- Host `model-price-quote` / `price_required` means steps 1–2 already missed.
`acn listen` answers: live provider catalog → **`--on-quote-model-prices` /
`ACN_ON_QUOTE_MODEL_PRICES`** (stdin `{model_ids,retry}`, stdout
`{model_prices:[...]}`, ≤20s) → optional `ACN_MODEL_PRICES` fallback.
Rates must be the Host market currency per million (no currency field).
`{model_prices:[...]}`, ≤20s) → optional `ACN_MODEL_PRICES`. Wire the hook
yourself. If catalog + hook + env miss, listen logs `quote_hook_missing` /
`quote_unpriced …`.
- Example only (not a Host-managed catalog): `scripts/comiclaw/quote-model-prices.sh`
— TokenPlan CNY stickers → Host USD via `COMICLAW_CNY_PER_USD` (default 7.2);
local miss → catalog / page lookup; optional `COMICLAW_MODEL_PRICES_MAX_AGE_HOURS`
treats an aged local file as empty. Copy the pattern; Host staff will not
edit another agent’s price file.
- `A2A accepted ≠ host processed` — if wake fails, check journal for
`wake_failed` and fall back to inbox / task list reconcile.
- Dedupe is in-process; restarting `acn-listen` clears the window.
Expand Down
40 changes: 20 additions & 20 deletions scripts/comiclaw/model_prices.seed.json
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
[
{
"model_id": "tencenttokenplan/kimi-k2.5",
"input_price_per_million": 4.0,
"output_price_per_million": 16.0
"input_price_cny_per_million": 4.0,
"output_price_cny_per_million": 16.0
},
{
"model_id": "tencenttokenplan/kimi-k2.6",
"input_price_per_million": 4.0,
"output_price_per_million": 16.0
"input_price_cny_per_million": 4.0,
"output_price_cny_per_million": 16.0
},
{
"model_id": "tencenttokenplan/glm-5",
"input_price_per_million": 1.0,
"output_price_per_million": 3.2
"input_price_cny_per_million": 1.0,
"output_price_cny_per_million": 3.2
},
{
"model_id": "tencenttokenplan/glm-5.1",
"input_price_per_million": 1.0,
"output_price_per_million": 3.2
"input_price_cny_per_million": 1.0,
"output_price_cny_per_million": 3.2
},
{
"model_id": "tencenttokenplan/minimax-m2.7",
"input_price_per_million": 2.1,
"output_price_per_million": 8.4
"input_price_cny_per_million": 2.1,
"output_price_cny_per_million": 8.4
},
{
"model_id": "tencenttokenplan/deepseek-v4-flash",
"input_price_per_million": 0.2,
"output_price_per_million": 0.4
"input_price_cny_per_million": 0.2,
"output_price_cny_per_million": 0.4
},
{
"model_id": "tencenttokenplan/deepseek-v4-flash-202605",
"input_price_per_million": 0.2,
"output_price_per_million": 0.4
"input_price_cny_per_million": 0.2,
"output_price_cny_per_million": 0.4
},
{
"model_id": "tencenttokenplan/deepseek-v4-pro",
"input_price_per_million": 4.5,
"output_price_per_million": 13.5
"input_price_cny_per_million": 4.5,
"output_price_cny_per_million": 13.5
},
{
"model_id": "tencenttokenplan/deepseek-v3",
"input_price_per_million": 2.0,
"output_price_per_million": 8.0
"input_price_cny_per_million": 2.0,
"output_price_cny_per_million": 8.0
},
{
"model_id": "tencenttokenplan/qwen3-max",
"input_price_per_million": 2.5,
"output_price_per_million": 10.0
"input_price_cny_per_million": 2.5,
"output_price_cny_per_million": 10.0
}
]
12 changes: 10 additions & 2 deletions scripts/comiclaw/quote-model-prices.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
#!/usr/bin/env bash
# ACN_ON_QUOTE_MODEL_PRICES hook for Comiclaw / TokenPlan.
# stdin: {"model_ids":[...],"retry":bool}
# EXAMPLE (Comiclaw / TokenPlan only) — not a Host-managed catalog.
# Other agents: copy the pattern; fetch your provider’s rates and report Host
# market currency yourself. Host staff will not edit your model_prices file.
# ACN_ON_QUOTE_MODEL_PRICES hook.
# stdin: {"model_ids":[...],"retry":bool}
# stdout: {"model_prices":[{model_id,input_price_per_million,output_price_per_million}]}
# Order when Host asks (Host book already missed): valid local → provider
# catalog/page → omit. Optional max-age empties a stale local file.
# Local / TokenPlan ¥ are CNY; stdout is Host market currency (USD).
# Optional: COMICLAW_CNY_PER_USD (default 7.2), COMICLAW_HOST_CURRENCY=USD|CNY,
# COMICLAW_MODEL_PRICES_MAX_AGE_HOURS (e.g. 168)
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -n "${ACN_PROVIDER_ENV_FILE:-}" ]]; then
Expand Down
Loading
Loading