@finchip.ai/foga 0.6.0-alpha.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -14
- package/dist/a2a/card.js +54 -0
- package/dist/a2a/server.js +276 -0
- package/dist/a2a/types.js +20 -0
- package/dist/build/bundle.js +2 -1
- package/dist/cli-cell.js +176 -16
- package/dist/cli.js +150 -5
- package/dist/core/world.js +17 -0
- package/dist/demo/birth.js +11 -5
- package/dist/demo/loop-d.js +39 -1
- package/dist/demo/quickstart.js +97 -11
- package/dist/genesis/operator-scope.js +1 -0
- package/dist/genesis/probe.js +23 -7
- package/dist/http/access.js +105 -0
- package/dist/http/server.js +84 -43
- package/dist/http/signed.js +136 -0
- package/dist/mcp/server.js +48 -1
- package/dist/node/advise.js +134 -0
- package/dist/node/config.js +55 -1
- package/dist/node/documents.js +298 -0
- package/dist/node/preflight.js +208 -0
- package/dist/node/propagation.js +95 -0
- package/dist/node/wallet.js +132 -0
- package/dist/tools/documents.js +74 -0
- package/dist/tools/index.js +3 -1
- package/dist/tools/propagation.js +35 -0
- package/dist/tools/tenders.js +22 -6
- package/dist/util/version.js +2 -0
- package/dist/world/sourcify.js +165 -0
- package/docs/a2a.md +31 -0
- package/docs/design-memo.md +23 -6
- package/docs/documents.md +25 -0
- package/docs/examples/README.md +2 -2
- package/docs/extending.md +49 -0
- package/docs/loop-d.md +11 -0
- package/docs/mcp.md +18 -11
- package/docs/node.md +35 -11
- package/docs/og-deployment.md +131 -0
- package/docs/propagation.md +41 -0
- package/docs/quickstart.md +36 -21
- package/docs/wallet.md +62 -0
- package/docs/world.md +12 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,11 @@ ERC-8004 (Trustless Agents) is a required dependency: ERC-8419, ERC-8434 and ERC
|
|
|
19
19
|
|
|
20
20
|
The project delivers three things from one kernel: the **Finch OG Agent** (the first running instance), the **FOGA node software** anyone can run with their own identity, model and wallet, and a **public access layer** (MCP, A2A, HTTP/CLI). It is built in phases; this repository has completed **Phase 2c: the genesis on a public chain**, on top of the Phase 2 genesis (T0–T9) and the Phase 1 world layer.
|
|
21
21
|
|
|
22
|
-
## Status: Phase
|
|
22
|
+
## Status: Phase 3 done (0.6.0, 2026-11-05) — the OG live at foga.finchip.ai, the MCP registry listing, the Sepolia Loop D with an outside agent; 3d-2 (the wallet, signed callers, the documents on chain, preflight, A2A, Sourcify), 3d-1 (quickstart, an outside agent's Loop D), 3c (Loop D with a model), 3b (MCP and HTTP), 3a (the kernel as a node) all done
|
|
23
|
+
|
|
24
|
+
**3d-3 (0.6.0, 2026-11-05).** Phase 3 closes with the reference cell served and the last acceptance run on the public chain. The OG was born fresh on Sepolia by the path any cell takes (`wallet new`, `cell new`, `cell init`, `docs publish`, `node serve` — one key, no migration; docs/og-deployment.md): agent 11261, its skill offered, its documents on chain, its fifteen contracts Sourcify-verified; it is served in host mode under systemd behind Caddy at `foga.finchip.ai` and listed in the MCP registry as `io.github.garyyang-finchip/foga`. Loop D with an outside agent ran on Sepolia and every invariant row passed (docs/loop-d.md, "On a public chain"); what the public chain taught is fixed in this release — a working directory is one run, role funding is priced with a floor, the settlement figure is the tender's own, a log scan is windowed. `foga cell advise` (`src/node/advise.ts`) tells a running cell what to do next — separate the one key into owner, operator and guardian first — and the recommended items print after a quickstart, a birth and at serve; the first run stays minimal on purpose. The covenant list is read off the world (an eleventh covenant appears with no code change) and the manifest carries a `services` slot — the maintainer's catalog, declinable, served by the reference cell at `/services.json` — docs/extending.md. 107 tests.
|
|
25
|
+
|
|
26
|
+
**3d-2 (0.6.0-alpha.7, 2026-11-03).** The second slice of 3d is what a cell needs to stand on its own among other agents, under one principle (docs/design-memo.md, section 10): the wallet is the first step, made by default and never a reason to stop; identity is one's own; only the genome replicates. `foga wallet new` (`src/node/wallet.ts`, docs/wallet.md) makes an agent's keys on its own machine — by default one key in a 0600 file under `~/.foga/wallet` — the identity that fills every role a cell needs; `--cell` for the separated owner/operator/guardian set — never overwritten, no network — and `foga quickstart` makes the default wallet on its first run and births every later cell with the same keys; `--wallet <dir>` on every `foga cell` and `foga node` command takes a role's key from there. A wallet is also a credential: `Authorization: FOGA-EIP712 <envelope>` (`src/http/signed.ts`, `src/http/access.ts`) is an EIP-712 signature in the cell's own domain — bound to one request and good once, or a session for a client with static headers — and the caller is the signing address at the level `access.signers` grants it (the cell's own operator or owner, an allow-list by address or ERC-8004 agent id, a KYA standing threshold, a default); `/api`, `/mcp` and `/a2a` take it, `foga wallet sign` prints it, and every cell's MCP server carries the prompt `become-a-cell` and the resource `foga://docs/quickstart` for an agent that meets it. `foga cell docs build | publish | status` (`src/node/documents.ts`, docs/documents.md) generates the cell's documents from one configuration — the ERC-8004 registration, the ERC-8434 AID document, the ERC-8444 descriptor, the ERC-8454 unit, the A2A agent card, `server.json`, a manifest with content hashes — serves them at `/documents/*` and `/.well-known/*`, and commits them on chain as the operator's keyed job (`setAgentURI`, `setDocumentURI`, `updateDescriptor`, `publishVersion`; only what differs is sent), read back and cross-checked; `docs_status` and `docs_publish` are the catalog's 30th and 31st capabilities. `foga node preflight` (`src/node/preflight.ts`) is the readiness reconciliation — the world, the account and its allow-list, the budget, the keys, the standing, the skills run in the worker, the documents against the chain, the public URL, the model — advisory, `--strict` for CI. `POST /a2a` (`src/a2a/`, docs/a2a.md) is the A2A 1.0 JSON-RPC binding with the card at `/.well-known/agent-card.json`: a quote for anyone, a tender taken for an act-level caller as one task over the catalog's jobs, `GetTask`, `ListTasks`, `CancelTask`, errors by the specification. `foga world verify --sourcify` (`src/world/sourcify.ts`, docs/world.md) submits every FOGA-deployed contract of a world with the standard JSON input of its own compiler run (`forge build` now leaves the build-info beside the artifacts), matched by runtime code; a local chain is a dry run, which CI runs over all 47. The OG instance's default is host mode (`foga node serve`: every entrance, the watchers, no model) with embedded mode (`foga node run`, the runner with a model) on demand. Seven new tests on anvil and offline (`wallet`, `signed`, `documents`, `a2a`, `preflight`, `sourcify`, and `quickstart` extended) are the gate; `foga quickstart` now ends with a cell whose documents are on chain, whose card answers, and whose A2A entrance quotes. Next, 3d-3: the OG deployment runbook, `foga.finchip.ai`, the registry listing, the Sepolia Sourcify run, the OG cell's migration to revision 2 and an outside agent's Loop D on Sepolia.
|
|
23
27
|
|
|
24
28
|
**3d-1 (0.6.0-alpha.6, 2026-11-02).** The package is on npm as `@finchip.ai/foga` (0.6.0-alpha.4 and alpha.5, the latter with the launcher fixed for Windows), so the first slice of 3d is the two things that make it usable from nothing. `foga quickstart` (`src/demo/quickstart.ts`, docs/quickstart.md) is one command to a live cell: anvil, the local world, a cell born with keys nobody held and its two skills, two tenders minted for it, the node served (watchers, HTTP API, MCP over Streamable HTTP), both entrances checked (the handshake and tool list over HTTP with an act-level key, and over stdio with the operator key), and the MCP client configurations — Claude Desktop, Claude Code, Cursor, and the HTTP form with URL and Bearer key — printed and written with every path filled in, under `~/.foga/quickstart`. Loop D gains `--model external` (docs/loop-d.md): the node is served instead of run, an act-level key is minted for whoever proposes, the orchestrator prints a brief (the entrance, the five steps of a tender, what the run does around the agent — nothing about the cases) and waits for the agent — any model, any runtime, a person with `curl` — with the same recovery drill, the same entrance checks and the same acceptance; the evidence names the agent and carries no runner ticks, the doctor marks a case the agent left alone `NotTested` rather than `Fail`, and `--compare` skips what one run did not reach. `test/loop-d-external.test.ts` drives it with a scripted agent through the MCP entrance and every verdict passes, the model-agnostic row included against the kept stub run. Smaller: `.gitattributes` keeps every text file LF (the launcher's shebang included), a `prepare` script compiles a checkout installed straight from git, the shared cell birth (`src/demo/birth.ts`) and anvil helpers (`src/demo/anvil.ts`) serve the demos, and the docs carry the PowerShell and Windows notes. Next in 3d: `foga node preflight`, `docs build/publish` with `setAgentURI`, the A2A card and inbound mapping, Sourcify, then the OG deployment runbook, the documentation site and the registry listing.
|
|
25
29
|
|
|
@@ -72,16 +76,26 @@ From Phase 1, unchanged in substance:
|
|
|
72
76
|
- **The doctor.** `foga doctor` reports five layers per contract — chain and code, interfaces, version match, configuration and permissions, semantic validation — with the verdicts Pass / Fail / Unknown / Not tested / Not applicable. Layers 1–4 probe the chain; layer 5 reads the genesis evidence. A doctor run can be exported as a JSON-RPC probe plan and replayed from results collected elsewhere, so a report never depends on which machine can reach which RPC.
|
|
73
77
|
- **Sepolia address book.** `worlds/sepolia.json` records every upstream reference deployment at its deployed revision, including the three seams the inventory found (docs/version-lock.md); the completed world and the doctor's report on it are the 2c files above.
|
|
74
78
|
|
|
75
|
-
What is next:
|
|
79
|
+
What is next: 3d-3 closes Phase 3 — the OG instance's move to a revision-2 account with public endpoints (`foga.finchip.ai`), its documents and sources verified on Sepolia, the registry listing and an outside agent's Loop D run on Sepolia; Phase 4 the hosted node and payments. The design is in docs/design-memo.md.
|
|
76
80
|
|
|
77
81
|
## Quick start
|
|
78
82
|
|
|
79
|
-
With Node 22.13+ and Foundry's `anvil` installed, one command births a cell on a local chain and prints the MCP client configuration to connect to it (docs/quickstart.md; in PowerShell quote the name: `npx -y '@finchip.ai/foga' quickstart`):
|
|
83
|
+
With Node 22.13+ and Foundry's `anvil` installed, one command makes your wallet (on the first run; `~/.foga/wallet`, reused after), births a cell with it on a local chain, publishes the cell's documents and prints the MCP client configuration to connect to it (docs/quickstart.md; in PowerShell quote the name: `npx -y '@finchip.ai/foga' quickstart`):
|
|
80
84
|
|
|
81
85
|
```bash
|
|
82
86
|
npx -y @finchip.ai/foga quickstart
|
|
83
87
|
```
|
|
84
88
|
|
|
89
|
+
The wallet alone, for an agent that will only call cells (docs/wallet.md), and a cell on a public chain born with it:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx -y @finchip.ai/foga wallet new # ~/.foga/wallet: one key (your identity, every role); prints the address to fund. --cell for the separated owner/operator/guardian set
|
|
93
|
+
foga cell new --wallet ~/.foga/wallet --name mine --world deployments/sepolia.json --store mine.sqlite --out mine.json
|
|
94
|
+
foga cell init --config mine.json --wallet ~/.foga/wallet
|
|
95
|
+
foga node preflight --config mine.json --wallet ~/.foga/wallet
|
|
96
|
+
foga node serve --config mine.json --wallet ~/.foga/wallet
|
|
97
|
+
```
|
|
98
|
+
|
|
85
99
|
From a checkout, three ways in, from most to least demanding:
|
|
86
100
|
|
|
87
101
|
```bash
|
|
@@ -164,18 +178,20 @@ deployments/ world files, doctor reports, genesis evidence (local output
|
|
|
164
178
|
prover/ the ZK proving sub-package: run.cjs bridge (credential and predicate proofs), build-predicate.mjs (the development ceremony), its own dependencies
|
|
165
179
|
zk/ circuits/ (kya_predicate_count_v1.circom) and build/ (verification_key.json committed; the proving key is rebuilt locally)
|
|
166
180
|
docker/, docker-compose.yml
|
|
167
|
-
|
|
181
|
+
deploy/ the OG host assets: foga-og.service (systemd, host mode), Caddyfile, og.env.example (docs/og-deployment.md)
|
|
182
|
+
site/ the documentation site for GitHub Pages (docs.foga.finchip.ai): index.html, llms.txt, CNAME
|
|
183
|
+
docs/ design memo, compatibility matrix, version lock, build, world, doctor, cell account, kernel, genesis, zk, node, mcp, loop-d, quickstart, wallet, documents, a2a, og-deployment; examples/ (client configurations)
|
|
168
184
|
scenarios/skills/ the two ERC-8338 skill packages of the genesis: foga-probe, finch-extract-verify
|
|
169
185
|
scenarios/fixtures/ the pinned document finch-extract-verify works on in T5 and T7
|
|
170
186
|
scripts/ install-solc.sh, install-foundry.sh, relay.mjs, pack-smoke.mjs (the npm tarball run from a clean install)
|
|
171
187
|
src/ TypeScript: build/ (vendor + bundle), world/ (plan + deploy), doctor/ (probe + evaluate),
|
|
172
188
|
core/ (foga-core: one module per ERC incl. acdf, adrf and swarm, measure, canonicalization, DAG-CBOR packager),
|
|
173
189
|
kernel/ (store, transaction manager, cell, kernel), genesis/ (roles, operator scope, probe, extract, skill worker, zk, scenario T0-T3, t3-zk, t4..t9, runner),
|
|
174
|
-
tools/ (the capability catalog: types, catalog, read, tenders, identity, admin), node/ (config, keys, node, policy, init, migrate, watchers),
|
|
175
|
-
mcp/ (the MCP server built from the catalog), http/ (the HTTP API, MCP over Streamable HTTP, API keys),
|
|
176
|
-
node/runner.ts and node/model/ (the runner, the stub and anthropic adapters), demo/ (the Loop D demander, orchestration and evidence),
|
|
190
|
+
tools/ (the capability catalog: types, catalog, read, tenders, identity, documents, admin), node/ (config, keys, wallet, node, policy, init, migrate, watchers, documents, preflight),
|
|
191
|
+
mcp/ (the MCP server built from the catalog), http/ (the HTTP API, MCP over Streamable HTTP, API keys, signed callers), a2a/ (the A2A 1.0 binding and card),
|
|
192
|
+
node/runner.ts and node/model/ (the runner, the stub and anthropic adapters), demo/ (the Loop D demander, orchestration and evidence, quickstart), world/sourcify.ts,
|
|
177
193
|
cli.ts, cli-cell.ts (foga cell / foga node)
|
|
178
|
-
test/ unit tests: bytecode and interface ids, skill packages, canonicalization, operation store, transaction semantics against an in-process chain (mock-chain.ts), keys, policy gates, catalog, cell config, swarm/ACDF vectors, extractor, measurement resolver, doctor layer 5; node-cli.test.ts (the Loop D cases through the CLI on anvil), mcp.test.ts (the entrances over MCP and HTTP on anvil; helpers/anvil-cell.ts births the cell), model.test.ts (the stub's table, the Anthropic adapter against a stand-in API, the Loop D verdicts), loop-d.test.ts (the unscripted run with the stub on anvil)
|
|
194
|
+
test/ unit tests: bytecode and interface ids, skill packages, canonicalization, operation store, transaction semantics against an in-process chain (mock-chain.ts), keys, policy gates, catalog, cell config, swarm/ACDF vectors, extractor, measurement resolver, doctor layer 5, sourcify; node-cli.test.ts (the Loop D cases through the CLI on anvil), mcp.test.ts (the entrances over MCP and HTTP on anvil; helpers/anvil-cell.ts births the cell), model.test.ts (the stub's table, the Anthropic adapter against a stand-in API, the Loop D verdicts), loop-d.test.ts (the unscripted run with the stub on anvil), loop-d-external.test.ts, quickstart.test.ts, wallet.test.ts, signed.test.ts, documents.test.ts, a2a.test.ts, preflight.test.ts (3d, on anvil)
|
|
179
195
|
vendor/ LOCK.json (sources are materialized here and gitignored)
|
|
180
196
|
worlds/ world configs: local.json (deploy), sepolia.json (attach)
|
|
181
197
|
```
|
|
@@ -188,27 +204,32 @@ foga build [--verify] [--solc-dir <dir>] [--only <repo,repo>]
|
|
|
188
204
|
foga bundle info
|
|
189
205
|
foga world deploy --world <config> [--rpc <url>] [--out <file>] [--resume] [--gas-multiplier <x>] [--allow-chain-mismatch]
|
|
190
206
|
foga world attach --world <config> [--out <file>]
|
|
207
|
+
foga world verify --world <world.json> --sourcify [--dry-run] [--only <id,id>] [--server <url>] [--out <dir>] [--build-info <dir>] source verification from the bundle's own compiler runs
|
|
191
208
|
foga doctor --world <world|config> [--attach] [--rpc <url>] [--out <report>] [--export-probe <file>] [--import-results <file>] [--genesis <evidence>]
|
|
192
209
|
foga probe relay-script
|
|
193
210
|
foga genesis run --world <world> [--rpc <url>] [--until T0..T9] [--store <file>] [--out <evidence>] [--zk auto|require|skip] [--crash-after <key>] [--crash-before <key>]
|
|
194
211
|
foga genesis status --world <world> [--store <file>]
|
|
195
212
|
foga keys new [--out <file>] | show | export --role <role> [--out <file>] | check
|
|
213
|
+
foga wallet new [--name <n> | --dir <dir>] [--cell] | show [--rpc <url>] | sign (--config <cfg> | --url <node> | --cell <acct> --chain-id <n>) [--role] [--ttl <s>] [--agent-id <id>] [--method --path [--body]]
|
|
214
|
+
an agent's own key (the first step; --cell for the separated owner/operator/guardian set), and a signed Authorization header for a cell
|
|
196
215
|
foga demo [--port <n>] [--keep] [--genesis <T0..T9>]
|
|
197
216
|
foga cell list [--public] | schema <capability>
|
|
198
|
-
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --
|
|
199
|
-
foga cell init --config <cell.config.json> [--capital-wei <n>] (owner key)
|
|
217
|
+
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --out <cell.config.json> (--wallet <dir> | --owner <addr> --operator <addr> --guardian <addr>)
|
|
218
|
+
foga cell init --config <cell.config.json> [--wallet <dir>] [--capital-wei <n>] (owner key)
|
|
200
219
|
foga cell migrate --config <cell.config.json> --to <new.config.json> [--operator <addr>] (owner key)
|
|
201
|
-
foga cell
|
|
220
|
+
foga cell docs build | publish | status --config <cell.config.json> [--wallet <dir>] the cell's documents, served and committed on chain (publish: operator key)
|
|
221
|
+
foga cell <capability> --config <cell.config.json> [--wallet <dir>] [--mode simulate|build|act] [--<field> <value> ...] [--json '<input>']
|
|
222
|
+
foga node preflight --config <cell.config.json> [--wallet <dir>] [--no-executors] [--no-network] [--out <report.json>] [--strict] the readiness reconciliation; sends nothing
|
|
202
223
|
foga node tick | watch --config <cell.config.json> [--interval <s>] (operator key alone)
|
|
203
|
-
foga node mcp --config <cell.config.json> MCP over stdio (= foga mcp; the operator with FOGA_OPERATOR_KEY[_FILE], reads and builds without)
|
|
204
|
-
foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch] the HTTP API, /mcp
|
|
224
|
+
foga node mcp --config <cell.config.json> MCP over stdio (= foga mcp; the operator with FOGA_OPERATOR_KEY[_FILE] or --wallet, reads and builds without)
|
|
225
|
+
foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch] host mode: the HTTP API, /mcp, /a2a, the documents, /healthz, the watchers; no model
|
|
205
226
|
foga node key new --config <cell.config.json> --id <id> [--level read|build|act] [--visibility public|caller|operator] [--note <text>]
|
|
206
227
|
foga node run --config <cell.config.json> [--model stub|anthropic] [--listen <host:port> | --no-http] [--interval <s>] [--once | --ticks <n>] [--transcripts <dir>]
|
|
207
228
|
the embedded node: server, watchers and the runner (operator key alone)
|
|
208
229
|
foga demo --loop-d [--model stub|anthropic|external] [--model-name <name>] [--seed <n>] [--no-recovery] anvil, the world, then Loop D judged by the doctor
|
|
209
230
|
foga loop-d run --world <world.json> --rpc <url> [--dir <dir>] [--out <evidence.json>] [--model] [--model-name] [--seed] [--no-recovery] [--listen] [--wait <s>] [--idle <s>]
|
|
210
231
|
foga doctor [--world <world.json>] --loop-d <evidence.json> [--compare <evidence2.json>] [--out <report.json>]
|
|
211
|
-
foga quickstart [--dir <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit] one command to a live cell and its client configuration
|
|
232
|
+
foga quickstart [--dir <dir>] [--wallet <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit] one command to a live cell (the wallet first) and its client configuration
|
|
212
233
|
```
|
|
213
234
|
|
|
214
235
|
During development the CLI runs from source with `npm run foga -- <args>`; `npm run build` compiles it to `dist/` for the `foga` binary.
|
package/dist/a2a/card.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SIGNED_SCHEME } from "../http/signed.js";
|
|
2
|
+
import { A2A_PROTOCOL_VERSION } from "./types.js";
|
|
3
|
+
/** The public endpoints of a cell: the configuration's, or derived from runtime.http.publicUrl. */
|
|
4
|
+
export function endpointsOf(node) {
|
|
5
|
+
const cfg = node.config;
|
|
6
|
+
const base = cfg.runtime.http.publicUrl?.replace(/\/+$/, "") ?? null;
|
|
7
|
+
return {
|
|
8
|
+
a2a: cfg.presence.endpoints.a2a ?? (base ? `${base}/a2a` : null),
|
|
9
|
+
mcp: cfg.presence.endpoints.mcp ?? (base ? `${base}/mcp` : null),
|
|
10
|
+
web: cfg.presence.endpoints.web ?? cfg.presence.homepage ?? base,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export async function buildAgentCard(node, o) {
|
|
14
|
+
const cfg = node.config;
|
|
15
|
+
const endpoints = endpointsOf(node);
|
|
16
|
+
const held = node.cell ? await node.heldSkills() : [];
|
|
17
|
+
const skills = held
|
|
18
|
+
.filter((h) => cfg.skills.offered.includes(h.name))
|
|
19
|
+
.map((h) => {
|
|
20
|
+
const m = h.pkg.manifest;
|
|
21
|
+
const version = m.skillVersion ?? String(h.pkg.version);
|
|
22
|
+
return {
|
|
23
|
+
id: `${h.name}@${version}`,
|
|
24
|
+
name: h.name,
|
|
25
|
+
description: m.summary ?? `The ERC-8338 skill ${h.name} (package ${h.packageHash})`,
|
|
26
|
+
tags: ["erc8338", "foga", h.name, ...(m.skillType ? [m.skillType] : [])],
|
|
27
|
+
examples: [
|
|
28
|
+
`{"foga":{"mode":"quote","skill":"${h.name}","input":{}}}`,
|
|
29
|
+
`{"foga":{"mode":"tender","tokenId":"<an ERC-8414 tender for ${h.name}, funded, with the hashlock committed>"}}`,
|
|
30
|
+
],
|
|
31
|
+
inputModes: ["application/json", "text/plain"],
|
|
32
|
+
outputModes: ["application/json"],
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
const card = {
|
|
36
|
+
name: cfg.presence.name,
|
|
37
|
+
description: cfg.presence.description || `FOGA cell ${cfg.name}`,
|
|
38
|
+
supportedInterfaces: endpoints.a2a ? [{ url: endpoints.a2a, protocolBinding: "JSONRPC", protocolVersion: A2A_PROTOCOL_VERSION }] : [],
|
|
39
|
+
...(cfg.presence.provider ? { provider: { organization: cfg.presence.provider.organization, url: cfg.presence.provider.url } } : {}),
|
|
40
|
+
version: o.version,
|
|
41
|
+
documentationUrl: "https://github.com/garyyang-finchip/foga#readme",
|
|
42
|
+
capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
|
|
43
|
+
securitySchemes: {
|
|
44
|
+
"api-key": { httpAuthSecurityScheme: { scheme: "Bearer", description: "An API key the cell's operator issued (foga node key new)." } },
|
|
45
|
+
"wallet-signature": { httpAuthSecurityScheme: { scheme: SIGNED_SCHEME, description: "An EIP-712 envelope signed by the caller's own wallet for this cell (docs/mcp.md, signed calls)." } },
|
|
46
|
+
},
|
|
47
|
+
securityRequirements: [],
|
|
48
|
+
defaultInputModes: ["text/plain", "application/json"],
|
|
49
|
+
defaultOutputModes: ["application/json", "text/plain"],
|
|
50
|
+
skills,
|
|
51
|
+
...(cfg.presence.image ? { iconUrl: cfg.presence.image } : {}),
|
|
52
|
+
};
|
|
53
|
+
return card;
|
|
54
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ACTION_RANK } from "../tools/index.js";
|
|
3
|
+
import { A2A_ERRORS, A2A_PROTOCOL_VERSION, TERMINAL_STATES } from "./types.js";
|
|
4
|
+
const TASK_PREFIX = "a2a.task.";
|
|
5
|
+
const MSG_PREFIX = "a2a.msg.";
|
|
6
|
+
const QUOTE_SKILL_LIMIT = 50;
|
|
7
|
+
const now = () => new Date().toISOString();
|
|
8
|
+
function errorInfo(reason, metadata = {}) {
|
|
9
|
+
return { "@type": "type.googleapis.com/google.rpc.ErrorInfo", reason, domain: "a2a-protocol.org", metadata: { ...metadata, timestamp: now() } };
|
|
10
|
+
}
|
|
11
|
+
export class A2AError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
data;
|
|
14
|
+
constructor(code, message, data) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.data = data;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function agentMessage(text, data, taskId, contextId) {
|
|
21
|
+
const parts = [{ text }];
|
|
22
|
+
if (data !== undefined)
|
|
23
|
+
parts.push({ data, mediaType: "application/json" });
|
|
24
|
+
return { messageId: randomUUID(), role: "ROLE_AGENT", parts, ...(taskId ? { taskId } : {}), ...(contextId ? { contextId } : {}) };
|
|
25
|
+
}
|
|
26
|
+
/** The `foga` request in a message: from a data part with a `foga` object, or a text part (a question in words → quote). */
|
|
27
|
+
export function fogaRequestOf(message) {
|
|
28
|
+
for (const p of message.parts) {
|
|
29
|
+
const d = p.data;
|
|
30
|
+
if (d && typeof d === "object" && d.foga && typeof d.foga === "object") {
|
|
31
|
+
const f = d.foga;
|
|
32
|
+
const mode = f.mode === "tender" ? "tender" : "quote";
|
|
33
|
+
return { mode, skill: typeof f.skill === "string" ? f.skill : undefined, input: f.input, tokenId: f.tokenId !== undefined ? String(f.tokenId) : undefined };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const text = message.parts.map((p) => p.text).filter((t) => typeof t === "string").join("\n").trim();
|
|
37
|
+
return { mode: "quote", text: text || undefined };
|
|
38
|
+
}
|
|
39
|
+
export class A2AServer {
|
|
40
|
+
node;
|
|
41
|
+
catalog;
|
|
42
|
+
log;
|
|
43
|
+
constructor(node, catalog, log = () => { }) {
|
|
44
|
+
this.node = node;
|
|
45
|
+
this.catalog = catalog;
|
|
46
|
+
this.log = log;
|
|
47
|
+
}
|
|
48
|
+
get store() {
|
|
49
|
+
return this.node.store;
|
|
50
|
+
}
|
|
51
|
+
load(id) {
|
|
52
|
+
return this.store.getFact(TASK_PREFIX + id);
|
|
53
|
+
}
|
|
54
|
+
save(t) {
|
|
55
|
+
this.store.setFact(TASK_PREFIX + t.task.id, t);
|
|
56
|
+
}
|
|
57
|
+
visible(t, caller) {
|
|
58
|
+
return caller.visibility === "operator" || t.caller === caller.id;
|
|
59
|
+
}
|
|
60
|
+
setState(t, state, message) {
|
|
61
|
+
t.task.status = { state, timestamp: now(), ...(message ? { message } : {}) };
|
|
62
|
+
if (message)
|
|
63
|
+
t.task.history = [...(t.task.history ?? []), message];
|
|
64
|
+
this.save(t);
|
|
65
|
+
}
|
|
66
|
+
/** The JSON-RPC dispatcher: one request in, one response out; the HTTP layer checks the version header and the media type. */
|
|
67
|
+
async handle(request, caller, version) {
|
|
68
|
+
const id = request?.id ?? null;
|
|
69
|
+
try {
|
|
70
|
+
if (!request || typeof request !== "object" || request.jsonrpc !== "2.0" || typeof request.method !== "string")
|
|
71
|
+
throw new A2AError(A2A_ERRORS.InvalidRequestError, "Request payload validation error");
|
|
72
|
+
const v = (version ?? "").trim() || "0.3";
|
|
73
|
+
if (v !== A2A_PROTOCOL_VERSION)
|
|
74
|
+
throw new A2AError(A2A_ERRORS.VersionNotSupportedError, `A2A version ${v} is not supported; this agent speaks ${A2A_PROTOCOL_VERSION} (send A2A-Version: ${A2A_PROTOCOL_VERSION})`, [errorInfo("VERSION_NOT_SUPPORTED", { requested: v, supported: [A2A_PROTOCOL_VERSION] })]);
|
|
75
|
+
const { method, params } = request;
|
|
76
|
+
let result;
|
|
77
|
+
switch (method) {
|
|
78
|
+
case "SendMessage":
|
|
79
|
+
result = await this.sendMessage(params, caller);
|
|
80
|
+
break;
|
|
81
|
+
case "GetTask":
|
|
82
|
+
result = this.getTask(params, caller);
|
|
83
|
+
break;
|
|
84
|
+
case "ListTasks":
|
|
85
|
+
result = this.listTasks((params ?? {}), caller);
|
|
86
|
+
break;
|
|
87
|
+
case "CancelTask":
|
|
88
|
+
result = await this.cancelTask(params, caller);
|
|
89
|
+
break;
|
|
90
|
+
case "SendStreamingMessage":
|
|
91
|
+
case "SubscribeToTask":
|
|
92
|
+
throw new A2AError(A2A_ERRORS.UnsupportedOperationError, "streaming is not supported by this agent (capabilities.streaming is false)");
|
|
93
|
+
case "GetExtendedAgentCard":
|
|
94
|
+
throw new A2AError(A2A_ERRORS.UnsupportedOperationError, "no extended agent card (capabilities.extendedAgentCard is false)");
|
|
95
|
+
case "CreateTaskPushNotificationConfig":
|
|
96
|
+
case "GetTaskPushNotificationConfig":
|
|
97
|
+
case "ListTaskPushNotificationConfigs":
|
|
98
|
+
case "DeleteTaskPushNotificationConfig":
|
|
99
|
+
throw new A2AError(A2A_ERRORS.PushNotificationNotSupportedError, "push notifications are not supported by this agent (capabilities.pushNotifications is false)");
|
|
100
|
+
default:
|
|
101
|
+
throw new A2AError(A2A_ERRORS.MethodNotFoundError, "Method not found");
|
|
102
|
+
}
|
|
103
|
+
return { jsonrpc: "2.0", id, result };
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
if (e instanceof A2AError)
|
|
107
|
+
return { jsonrpc: "2.0", id, error: { code: e.code, message: e.message, ...(e.data ? { data: e.data } : {}) } };
|
|
108
|
+
this.log(`a2a: ${e.message}`);
|
|
109
|
+
return { jsonrpc: "2.0", id, error: { code: A2A_ERRORS.InternalError, message: "Internal error" } };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------- SendMessage
|
|
113
|
+
async sendMessage(params, caller) {
|
|
114
|
+
const m = params?.message;
|
|
115
|
+
if (!m || typeof m !== "object" || typeof m.messageId !== "string" || !Array.isArray(m.parts) || m.parts.length === 0)
|
|
116
|
+
throw new A2AError(A2A_ERRORS.InvalidParamsError, "Invalid parameters", [{ "@type": "type.googleapis.com/google.rpc.BadRequest", fieldViolations: [{ field: "message", description: "messageId and at least one part are required" }] }]);
|
|
117
|
+
for (const p of m.parts) {
|
|
118
|
+
if (p.raw !== undefined || p.url !== undefined)
|
|
119
|
+
throw new A2AError(A2A_ERRORS.ContentTypeNotSupportedError, "file parts are not accepted; send text/plain or an application/json data part");
|
|
120
|
+
if (p.mediaType && !/^(text\/plain|application\/json)\b/.test(p.mediaType))
|
|
121
|
+
throw new A2AError(A2A_ERRORS.ContentTypeNotSupportedError, `media type ${p.mediaType} is not accepted; text/plain or application/json`);
|
|
122
|
+
}
|
|
123
|
+
// the same message from the same caller is the same task (3.3.1: messageId detects duplicates)
|
|
124
|
+
const dedupeKey = `${MSG_PREFIX}${caller.id}:${m.messageId}`;
|
|
125
|
+
const known = this.store.getFact(dedupeKey);
|
|
126
|
+
if (known) {
|
|
127
|
+
const t = this.load(known);
|
|
128
|
+
if (t)
|
|
129
|
+
return { task: this.publicTask(t, params.configuration?.historyLength) };
|
|
130
|
+
}
|
|
131
|
+
// a message to an existing task: input for one that asked, nothing for a terminal one
|
|
132
|
+
if (m.taskId) {
|
|
133
|
+
const t = this.load(m.taskId);
|
|
134
|
+
if (!t || !this.visible(t, caller))
|
|
135
|
+
throw new A2AError(A2A_ERRORS.TaskNotFoundError, "Task not found", [errorInfo("TASK_NOT_FOUND", { taskId: m.taskId })]);
|
|
136
|
+
if (TERMINAL_STATES.includes(t.task.status.state))
|
|
137
|
+
throw new A2AError(A2A_ERRORS.UnsupportedOperationError, `task ${m.taskId} is ${t.task.status.state} and accepts no further message`);
|
|
138
|
+
t.messageIds.push(m.messageId);
|
|
139
|
+
t.task.history = [...(t.task.history ?? []), m];
|
|
140
|
+
this.store.setFact(dedupeKey, t.task.id);
|
|
141
|
+
await this.work(t, m, caller);
|
|
142
|
+
return { task: this.publicTask(t, params.configuration?.historyLength) };
|
|
143
|
+
}
|
|
144
|
+
const task = { task: { id: randomUUID(), contextId: m.contextId ?? randomUUID(), status: { state: "TASK_STATE_SUBMITTED", timestamp: now() }, history: [m], artifacts: [], metadata: { caller: caller.id, cell: this.node.config.name } }, caller: caller.id, messageIds: [m.messageId], createdAt: now(), jobs: {} };
|
|
145
|
+
this.save(task);
|
|
146
|
+
this.store.setFact(dedupeKey, task.task.id);
|
|
147
|
+
await this.work(task, m, caller);
|
|
148
|
+
return { task: this.publicTask(task, params.configuration?.historyLength) };
|
|
149
|
+
}
|
|
150
|
+
/** Decide what a message asks for and do it: a quote at once, a tender's work started. */
|
|
151
|
+
async work(t, m, caller) {
|
|
152
|
+
const req = fogaRequestOf(m);
|
|
153
|
+
if (req.mode === "quote") {
|
|
154
|
+
await this.quote(t, req);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const tokenId = req.tokenId ?? t.tokenId;
|
|
158
|
+
if (!tokenId) {
|
|
159
|
+
this.setState(t, "TASK_STATE_INPUT_REQUIRED", agentMessage("Which tender? Send a data part {\"foga\":{\"mode\":\"tender\",\"tokenId\":\"<id>\"}} naming an ERC-8414 tender that exists, is funded, and has its answer committed to the hashlock verifier.", { foga: { need: ["tokenId"] } }, t.task.id, t.task.contextId));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (ACTION_RANK[caller.level] < ACTION_RANK.act) {
|
|
163
|
+
this.setState(t, "TASK_STATE_REJECTED", agentMessage("This caller may not ask the cell to act: the tender mode needs an act-level credential (an API key the operator issued, or a wallet signature the cell's policy grants). A quote is open to everyone.", { foga: { gate: "permission", callerLevel: caller.level } }, t.task.id, t.task.contextId));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
t.tokenId = tokenId;
|
|
167
|
+
this.setState(t, "TASK_STATE_WORKING", agentMessage(`Working tender ${tokenId}: pre-check, run, submit. Settlement is the node's own duty afterwards.`, undefined, t.task.id, t.task.contextId));
|
|
168
|
+
// the work continues after the response (3.3.3); GetTask reads the result
|
|
169
|
+
void this.takeTender(t, tokenId, caller).catch((e) => {
|
|
170
|
+
this.log(`a2a: task ${t.task.id}: ${e.message}`);
|
|
171
|
+
const fresh = this.load(t.task.id) ?? t;
|
|
172
|
+
this.setState(fresh, "TASK_STATE_FAILED", agentMessage(`The work failed: ${e.message}`, undefined, t.task.id, t.task.contextId));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async quote(t, req) {
|
|
176
|
+
const cfg = this.node.config;
|
|
177
|
+
const held = this.node.cell ? await this.node.heldSkills() : [];
|
|
178
|
+
const offered = held.filter((h) => cfg.skills.offered.includes(h.name)).slice(0, QUOTE_SKILL_LIMIT);
|
|
179
|
+
const terms = { acceptance: "hashlock", verifier: this.node.acceptance.hashlock, asset: "native", minRewardWei: cfg.policy.tenders.minRewardWei, minSubmitWindowSeconds: cfg.policy.tenders.minSubmitWindowSeconds, minSettleWindowSeconds: cfg.policy.tenders.minSettleWindowSeconds, counterpartyMinLevel: cfg.policy.tenders.counterpartyMinLevel };
|
|
180
|
+
const how = "Mint an ERC-8414 tender for the skill with these terms (tender_mint_build gives the calls for your own account), fund it, commit the answer hash to the hashlock verifier, then send {\"foga\":{\"mode\":\"tender\",\"tokenId\":\"<id>\"}} with an act-level credential.";
|
|
181
|
+
if (req.skill && !offered.some((h) => h.name === req.skill)) {
|
|
182
|
+
this.setState(t, "TASK_STATE_REJECTED", agentMessage(`The cell does not offer a skill named ${req.skill}. Offered: ${offered.map((h) => h.name).join(", ") || "none"}.`, { foga: { quote: { skill: req.skill, offered: false, available: offered.map((h) => h.name) } } }, t.task.id, t.task.contextId));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const chosen = req.skill ? offered.filter((h) => h.name === req.skill) : offered;
|
|
186
|
+
const quote = { cell: cfg.name, account: this.node.cell?.address ?? null, skills: chosen.map((h) => ({ name: h.name, version: String(h.pkg.manifest.skillVersion ?? h.pkg.version), packageHash: h.packageHash, tokenId: h.tokenId.toString(), summary: h.pkg.manifest.summary ?? null })), terms, ...(req.input !== undefined ? { inputEchoed: req.input } : {}), how };
|
|
187
|
+
const artifact = { artifactId: randomUUID(), name: "quote", description: "What the cell can do and on what terms", parts: [{ data: { foga: { quote } }, mediaType: "application/json" }] };
|
|
188
|
+
t.task.artifacts = [artifact];
|
|
189
|
+
const text = req.text ? `You asked: ${req.text.slice(0, 200)}. ` : "";
|
|
190
|
+
this.setState(t, "TASK_STATE_COMPLETED", agentMessage(`${text}The cell offers ${chosen.length} skill(s)${chosen.length ? `: ${chosen.map((h) => h.name).join(", ")}` : ""}; the terms are in the quote artifact. Nothing ran and nothing touched the chain.`, undefined, t.task.id, t.task.contextId));
|
|
191
|
+
}
|
|
192
|
+
/** The tender road: the catalog decides every step as the caller; the task records the decisions. */
|
|
193
|
+
async takeTender(t, tokenId, caller) {
|
|
194
|
+
const reject = (text, data) => this.setState(this.load(t.task.id) ?? t, "TASK_STATE_REJECTED", agentMessage(text, data, t.task.id, t.task.contextId));
|
|
195
|
+
const cancelled = () => Boolean(this.load(t.task.id)?.cancelRequested);
|
|
196
|
+
const pre = await this.catalog.invoke(this.node, "tender_precheck", { tokenId }, caller, { mode: "act" });
|
|
197
|
+
if (!pre.ok)
|
|
198
|
+
return reject(`Tender ${tokenId} refused at ${pre.gate}: ${pre.reason}`, { foga: { gate: pre.gate, reason: pre.reason, decisionId: pre.decisionId ?? null } });
|
|
199
|
+
const view = pre.output.tender;
|
|
200
|
+
if (!view.task)
|
|
201
|
+
return reject(`Tender ${tokenId} has no readable task document`, { foga: { gate: "input" } });
|
|
202
|
+
if (cancelled())
|
|
203
|
+
return this.setState(this.load(t.task.id), "TASK_STATE_CANCELED", agentMessage("Cancelled before the run.", undefined, t.task.id, t.task.contextId));
|
|
204
|
+
const run = await this.catalog.invoke(this.node, "skill_run", { skill: view.task.skill, input: view.task.input, tokenId }, caller, { mode: "act" });
|
|
205
|
+
if (!run.ok)
|
|
206
|
+
return reject(`The run for tender ${tokenId} was refused at ${run.gate}: ${run.reason}`, { foga: { gate: run.gate, reason: run.reason, decisionId: run.decisionId ?? null } });
|
|
207
|
+
const stored = this.load(t.task.id) ?? t;
|
|
208
|
+
stored.jobs.run = run.jobId;
|
|
209
|
+
this.save(stored);
|
|
210
|
+
if (cancelled())
|
|
211
|
+
return this.setState(this.load(t.task.id), "TASK_STATE_CANCELED", agentMessage("Cancelled before the submission.", undefined, t.task.id, t.task.contextId));
|
|
212
|
+
const out = run.output;
|
|
213
|
+
const sub = await this.catalog.invoke(this.node, "tender_submit", { tokenId, resultHash: out.resultHash, runReceipt: out.runReceipt }, caller, { mode: "act" });
|
|
214
|
+
const latest = this.load(t.task.id) ?? t;
|
|
215
|
+
if (!sub.ok) {
|
|
216
|
+
latest.jobs.submit = sub.jobId;
|
|
217
|
+
this.save(latest);
|
|
218
|
+
return reject(`The submission for tender ${tokenId} was refused at ${sub.gate}: ${sub.reason}`, { foga: { gate: sub.gate, reason: sub.reason, decisionId: sub.decisionId ?? null, jobId: sub.jobId ?? null } });
|
|
219
|
+
}
|
|
220
|
+
latest.jobs.submit = sub.jobId;
|
|
221
|
+
const result = sub.output;
|
|
222
|
+
latest.task.artifacts = [{ artifactId: randomUUID(), name: "submission", description: "The cell's submission on the tender", parts: [{ data: { foga: { tokenId, resultHash: out.resultHash, answerHash: out.answerHash, submissionId: result.submissionId ?? null, txHash: result.txHash ?? null, jobId: sub.jobId, decisionId: sub.decisionId ?? null, operations: sub.operations ?? [] } }, mediaType: "application/json" }] }];
|
|
223
|
+
this.setState(latest, "TASK_STATE_COMPLETED", agentMessage(`Tender ${tokenId} submitted (job ${sub.jobId}); the node settles it once the chain allows.`, undefined, t.task.id, t.task.contextId));
|
|
224
|
+
}
|
|
225
|
+
// ---------------------------------------------------------------------------- GetTask, ListTasks, CancelTask
|
|
226
|
+
getTask(params, caller) {
|
|
227
|
+
if (!params || typeof params.id !== "string")
|
|
228
|
+
throw new A2AError(A2A_ERRORS.InvalidParamsError, "Invalid parameters", [{ "@type": "type.googleapis.com/google.rpc.BadRequest", fieldViolations: [{ field: "id", description: "the task id is required" }] }]);
|
|
229
|
+
const t = this.load(params.id);
|
|
230
|
+
if (!t || !this.visible(t, caller))
|
|
231
|
+
throw new A2AError(A2A_ERRORS.TaskNotFoundError, "Task not found", [errorInfo("TASK_NOT_FOUND", { taskId: params.id })]);
|
|
232
|
+
return this.publicTask(t, params.historyLength);
|
|
233
|
+
}
|
|
234
|
+
listTasks(params, caller) {
|
|
235
|
+
const all = Object.values(this.store.facts(TASK_PREFIX));
|
|
236
|
+
let tasks = all.filter((t) => this.visible(t, caller));
|
|
237
|
+
if (params.contextId)
|
|
238
|
+
tasks = tasks.filter((t) => t.task.contextId === params.contextId);
|
|
239
|
+
if (params.status)
|
|
240
|
+
tasks = tasks.filter((t) => t.task.status.state === params.status);
|
|
241
|
+
if (params.statusTimestampAfter)
|
|
242
|
+
tasks = tasks.filter((t) => (t.task.status.timestamp ?? "") > params.statusTimestampAfter);
|
|
243
|
+
tasks.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
|
244
|
+
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 50));
|
|
245
|
+
const offset = params.pageToken ? Number(params.pageToken) || 0 : 0;
|
|
246
|
+
const page = tasks.slice(offset, offset + pageSize);
|
|
247
|
+
return { tasks: page.map((t) => this.publicTask(t, params.historyLength, params.includeArtifacts ?? false)), nextPageToken: offset + pageSize < tasks.length ? String(offset + pageSize) : "", pageSize, totalSize: tasks.length };
|
|
248
|
+
}
|
|
249
|
+
async cancelTask(params, caller) {
|
|
250
|
+
if (!params || typeof params.id !== "string")
|
|
251
|
+
throw new A2AError(A2A_ERRORS.InvalidParamsError, "Invalid parameters");
|
|
252
|
+
const t = this.load(params.id);
|
|
253
|
+
if (!t || !this.visible(t, caller))
|
|
254
|
+
throw new A2AError(A2A_ERRORS.TaskNotFoundError, "Task not found", [errorInfo("TASK_NOT_FOUND", { taskId: params.id })]);
|
|
255
|
+
if (TERMINAL_STATES.includes(t.task.status.state))
|
|
256
|
+
throw new A2AError(A2A_ERRORS.TaskNotCancelableError, `task ${params.id} is ${t.task.status.state}`, [errorInfo("TASK_NOT_CANCELABLE", { taskId: params.id, state: t.task.status.state })]);
|
|
257
|
+
// a submission on its way to the chain is not undone by a cancellation: what was broadcast stays
|
|
258
|
+
if (t.jobs.submit)
|
|
259
|
+
throw new A2AError(A2A_ERRORS.TaskNotCancelableError, `task ${params.id} has submitted on chain (job ${t.jobs.submit}); a broadcast is not undone`, [errorInfo("TASK_NOT_CANCELABLE", { taskId: params.id, jobId: t.jobs.submit })]);
|
|
260
|
+
if (t.task.status.state === "TASK_STATE_WORKING") {
|
|
261
|
+
t.cancelRequested = true;
|
|
262
|
+
this.save(t);
|
|
263
|
+
return this.publicTask(t);
|
|
264
|
+
}
|
|
265
|
+
this.setState(t, "TASK_STATE_CANCELED", agentMessage("Cancelled.", undefined, t.task.id, t.task.contextId));
|
|
266
|
+
return this.publicTask(t);
|
|
267
|
+
}
|
|
268
|
+
publicTask(t, historyLength, artifacts = true) {
|
|
269
|
+
const task = { ...t.task };
|
|
270
|
+
if (historyLength !== undefined && task.history)
|
|
271
|
+
task.history = historyLength <= 0 ? [] : task.history.slice(-historyLength);
|
|
272
|
+
if (!artifacts)
|
|
273
|
+
delete task.artifacts;
|
|
274
|
+
return task;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const TERMINAL_STATES = ["TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"];
|
|
2
|
+
/** JSON-RPC error codes of the binding (specification section 5.4 and 9.5). */
|
|
3
|
+
export const A2A_ERRORS = {
|
|
4
|
+
JSONParseError: -32700,
|
|
5
|
+
InvalidRequestError: -32600,
|
|
6
|
+
MethodNotFoundError: -32601,
|
|
7
|
+
InvalidParamsError: -32602,
|
|
8
|
+
InternalError: -32603,
|
|
9
|
+
TaskNotFoundError: -32001,
|
|
10
|
+
TaskNotCancelableError: -32002,
|
|
11
|
+
PushNotificationNotSupportedError: -32003,
|
|
12
|
+
UnsupportedOperationError: -32004,
|
|
13
|
+
ContentTypeNotSupportedError: -32005,
|
|
14
|
+
InvalidAgentResponseError: -32006,
|
|
15
|
+
ExtendedAgentCardNotConfiguredError: -32007,
|
|
16
|
+
ExtensionSupportRequiredError: -32008,
|
|
17
|
+
VersionNotSupportedError: -32009,
|
|
18
|
+
};
|
|
19
|
+
export const A2A_PROTOCOL_VERSION = "1.0";
|
|
20
|
+
export const A2A_WELL_KNOWN = "/.well-known/agent-card.json";
|
package/dist/build/bundle.js
CHANGED
|
@@ -35,7 +35,8 @@ function solcBinary(profiles, version, solcDir) {
|
|
|
35
35
|
function forgeBuild(projectDir, outDir, skip, solcPath, extraArgs = []) {
|
|
36
36
|
rmrf(outDir);
|
|
37
37
|
ensureDir(outDir);
|
|
38
|
-
|
|
38
|
+
// the build-info files (the standard JSON input and output of each compiler run) stay beside the artifacts: `foga world verify --sourcify` submits them
|
|
39
|
+
const args = ["build", "--ast", "--no-cache", "--out", outDir, "--build-info", "--build-info-path", join(outDir, "build-info"), ...extraArgs];
|
|
39
40
|
for (const s of skip)
|
|
40
41
|
args.push("--skip", s);
|
|
41
42
|
if (solcPath)
|