Skip to content

Commit 2cd4119

Browse files
authored
Merge pull request #213 from Deep-CodeAI/feat/4518-oasf-record-export
feat(#4518): OASF 1.0.0 record export — toOasfRecord() (AGNTCY interop, slice 1)
2 parents e915bcf + 90b8ac4 commit 2cd4119

11 files changed

Lines changed: 499 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ All notable changes to Agents.KT are documented here. The format follows [Keep a
44

55
## [Unreleased]
66

7+
### Added — OASF 1.0.0 record export: `toOasfRecord()` (#4518, PRD §12.6) — AGNTCY interop, slice 1
8+
9+
`agent.toOasfRecord(version, authors, locators, …)` emits an [OASF](https://github.com/agntcy/oasf) 1.0.0
10+
record — AGNTCY's content-addressed discovery metadata — the **third discovery exporter** beside the A2A
11+
AgentCard (`toAgentCard()`, §12.5) and native `agent.json` (`toAgentJson()`, §12.2), and the first piece of
12+
the AGNTCY epic (#4517: OASF + DIR + Identity-verify). The native typed agent stays the source of truth; this
13+
is a projection over it. OASF skills are taxonomy entries, not free text: a skill becomes an OASF `skills[]`
14+
entry only when annotated with `.oasf("agent_orchestration/multi_agent_planning")`, resolved to its uid via
15+
the vendored `OasfTaxonomy` (a `path → uid` lookup — OASF uids are explicitly assigned per node, not a single
16+
formula; un-annotated/unknown skills are omitted with a logged warning). Deterministic and byte-stable:
17+
`createdAt`/`authors`/`locators` are caller-supplied (no hidden `now()`). `toAgentJson()` gained the same
18+
optional provenance fields additively (`metadata.authors`, `metadata.createdAt`, `spec.locators`) — existing
19+
callers serialize byte-identically. New package `agents_engine.agntcy` (`toOasfRecord`, `OasfTaxonomy`,
20+
`OasfLocator`). **Slice 1** seeds the confirmed core of the skills taxonomy; **slice 2** (follow-up in #4518)
21+
vendors the complete `agntcy/oasf` trees + a build-time cross-check. 5 tests. Record signing, OASF
22+
import/validate, the DIR client, and Identity-verify are the remaining #4517 subtasks.
23+
724
### Added — `NlWebServer`: serve agents.kt as an NLWeb endpoint (#4542, PRD §12.9)
825

926
The serve side of NLWeb (the `nlwebSearch` tool, #4541, is the consume side). `NlWebServer.from(agent).start()`

docs/prd.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2855,7 +2855,7 @@ Any node in the delegation tree can be exported as an A2A endpoint:
28552855
project.toAgentCard(url = "https://api.deep-code.ai/agents/project")
28562856
```
28572857

2858-
### 12.6 AGNTCY Interoperability *(planned)*
2858+
### 12.6 AGNTCY Interoperability *(in progress — OASF record export shipped, slice 1; DIR + Identity-verify planned)*
28592859

28602860
[AGNTCY](https://github.com/agntcy) — the Linux Foundation "Internet of Agents" collective (Cisco/Outshift-led) — is the second cross-vendor interop stack alongside Google A2A (§12.5). Agents.KT targets **both**: A2A is the wire/invocation standard; AGNTCY adds a content-addressed **directory** and a **trust** layer. The native, typed `agent.json` (§12.2) stays the source of truth; AGNTCY support is a set of **exporters/clients over it**, exactly parallel to `toAgentCard()`.
28612861

@@ -2871,19 +2871,20 @@ project.toAgentCard(url = "https://api.deep-code.ai/agents/project")
28712871

28722872
So AGNTCY interop = **OASF + DIR + Identity-verify**, riding on the A2A we already do.
28732873

2874-
**OASF record export/import.** A third discovery exporter beside A2A:
2874+
**OASF record export/import.** A third discovery exporter beside A2A**shipped (slice 1, #4518)**:
28752875

28762876
```kotlin
28772877
val record = specMaster.toOasfRecord(
28782878
version = "2.0.0",
28792879
authors = listOf("K.Skobeltsyn <konstantin@skobeltsyn.com>"),
2880-
locators = listOf(Locator.sourceCode("https://github.com/Deep-CodeAI/Agents.KT")),
2880+
locators = listOf(OasfLocator("source_code", listOf("https://github.com/Deep-CodeAI/Agents.KT"))),
2881+
createdAt = "2026-06-15T00:00:00Z", // caller-supplied — keeps the record byte-stable
28812882
)
2882-
// → OASF 1.0.0 JSON: name, version, schema_version, authors, created_at,
2883-
// skills:[{name,id}], domains:[{name,id}], locators:[...], modules:[]
2883+
// → OASF 1.0.0 JSON: name, version, schema_version, description?, authors, created_at?,
2884+
// skills:[{name,id}], domains:[{name,id}], locators:[{type,urls}], modules:[], annotations
28842885
```
28852886

2886-
The one real engineering cost is the **skills/domains taxonomy**: OASF skills are not free text — each is `{name: "agent_orchestration/task_decomposition", id: 1001}`, where `id` is digit-concatenation of the hierarchy UIDs. No JVM SDK and no fuzzy matcher exist. Plan: **vendor** the `schema/skills` + `schema/domains` trees and compute IDs locally (offline, reproducible), with the hosted schema server (`schema.oasf.outshift.com/api/skills`) as a validation cross-check. Free-form agent skills map via an opt-in `.oasf("agent_orchestration/task_decomposition")` annotation; un-annotated skills export under a sensible default and a validation warning. Record **signing** (Sigstore/cosign over OCI) is external to the record JSON — a later optional integration, not part of the serializer.
2887+
The one real engineering cost is the **skills/domains taxonomy**: OASF skills are not free text — each is `{name: "agent_orchestration/multi_agent_planning", id: 1003}`. Note the `id` is **not** a single digit-concatenation formula as first assumed — the uids are *explicitly assigned per node* (top-level categories are multiples of 100, but level-2 is `category + n` while level-3 is `level2*100 + nn`), so the correct mechanism is a **vendored `path → uid` lookup**, not a computation. Plan: **vendor** the `schema/skills` + `schema/domains` trees (resource TSVs) and resolve IDs by lookup, with the hosted schema server (`schema.oasf.outshift.com/api/skills`) as a build-time validation cross-check. Free-form agent skills map via an opt-in `.oasf("agent_orchestration/multi_agent_planning")` annotation on the skill; un-annotated (and unknown-path) skills are omitted from the OASF `skills[]` with a logged warning (they remain in the free-form `agent.json`). Slice 1 ships the exporter + the confirmed core of the taxonomy; **slice 2** vendors the complete trees + the cross-check. Record **signing** (Sigstore/cosign over OCI) is external to the record JSON — a later optional integration, not part of the serializer.
28872888

28882889
**DIR client.** `buf generate buf.build/agntcy/dir` → grpc-kotlin stubs for `StoreService.{Push,Pull,Lookup}` (CID-addressed) and `RoutingService`/`SearchService`. DIR carries our OASF record as an opaque `google.protobuf.Struct`, so the JSON is enough — no OASF protos required. Auth is layered and optional (insecure dev / SPIFFE / OIDC bearer). Targets both self-hosted (`localhost:8888`) and the hosted network (`prod.api.ads.outshift.io`, auth-gated via hub login).
28892890

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package agents_engine.agntcy
2+
3+
/**
4+
* `agents_engine/agntcy/OasfLocator.kt` — #4518 (PRD §12.6). An OASF record `locator`: where the
5+
* agent's artifact can be obtained. [type] is the OASF locator type (e.g. `"source_code"`,
6+
* `"docker_image"`, `"binary"`, `"helm_chart"`); [urls] are the addresses for that type. Serialized
7+
* verbatim into the OASF record's `locators[]` and (additively) into `agent.json`'s `spec.locators`.
8+
*/
9+
data class OasfLocator(
10+
val type: String,
11+
val urls: List<String>,
12+
)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package agents_engine.agntcy
2+
3+
import agents_engine.core.Agent
4+
import agents_engine.mcp.McpJson
5+
import java.util.logging.Logger
6+
7+
private val log: Logger = Logger.getLogger("agents_engine.agntcy.OasfRecord")
8+
9+
/**
10+
* `agents_engine/agntcy/OasfRecord.kt` — #4518 (PRD §12.6). Serialize an [Agent] to an
11+
* [OASF](https://github.com/agntcy/oasf) 1.0.0 record: AGNTCY's content-addressed discovery metadata,
12+
* the third exporter beside the A2A AgentCard (`toAgentCard()`, §12.5) and native `agent.json`
13+
* (`toAgentJson`, §12.2). The native typed agent stays the source of truth; this is a projection over
14+
* it, exactly parallel to `toAgentCard()`.
15+
*
16+
* **Skills are taxonomy entries, not free text.** Only skills annotated with `.oasf("path")` (see
17+
* [agents_engine.core.Skill.oasf]) become OASF `skills[]` — each resolved to its uid via
18+
* [OasfTaxonomy]. Un-annotated skills, and annotated paths not in the vendored taxonomy, are omitted
19+
* with a logged warning (they're still in `agent.json`, which carries free-form skills).
20+
*
21+
* **Determinism.** Keys are emitted in a fixed order and the same inputs serialize byte-identically.
22+
* Wall-clock fields ([createdAt]) and authorship ([authors], [locators]) are caller-supplied rather
23+
* than sampled, so the record stays reproducible (no hidden `now()`).
24+
*
25+
* Record signing (Sigstore/cosign over OCI) is external to the record JSON and is deferred (PRD §12.6).
26+
*/
27+
fun Agent<*, *>.toOasfRecord(
28+
version: String,
29+
authors: List<String> = emptyList(),
30+
locators: List<OasfLocator> = emptyList(),
31+
domains: List<String> = emptyList(),
32+
description: String? = null,
33+
createdAt: String? = null,
34+
annotations: Map<String, String> = emptyMap(),
35+
): String {
36+
val skillRecords = skills.values
37+
.sortedBy { it.name }
38+
.mapNotNull { skill ->
39+
val path = skill.oasfPath
40+
if (path == null) {
41+
log.warning("OASF: skill \"${skill.name}\" has no .oasf(path) — omitted from OASF skills[].")
42+
return@mapNotNull null
43+
}
44+
val uid = OasfTaxonomy.skillUid(path)
45+
if (uid == null) {
46+
log.warning("OASF: skill \"${skill.name}\" path \"$path\" not in vendored OASF taxonomy — omitted.")
47+
return@mapNotNull null
48+
}
49+
linkedMapOf<String, Any?>("name" to path, "id" to uid)
50+
}
51+
52+
val domainRecords = domains
53+
.sorted()
54+
.mapNotNull { path ->
55+
val uid = OasfTaxonomy.domainUid(path)
56+
if (uid == null) {
57+
log.warning("OASF: domain \"$path\" is not in the vendored OASF taxonomy — omitted.")
58+
return@mapNotNull null
59+
}
60+
linkedMapOf<String, Any?>("name" to path, "id" to uid)
61+
}
62+
63+
val record = LinkedHashMap<String, Any?>()
64+
record["name"] = name
65+
record["version"] = version
66+
record["schema_version"] = OasfTaxonomy.SCHEMA_VERSION
67+
if (description != null) record["description"] = description
68+
record["authors"] = authors
69+
if (createdAt != null) record["created_at"] = createdAt
70+
record["skills"] = skillRecords
71+
record["domains"] = domainRecords
72+
record["locators"] = locators.map { linkedMapOf<String, Any?>("type" to it.type, "urls" to it.urls) }
73+
record["modules"] = emptyList<Any?>()
74+
record["annotations"] = annotations
75+
76+
return McpJson.encode(record)
77+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package agents_engine.agntcy
2+
3+
/**
4+
* `agents_engine/agntcy/OasfTaxonomy.kt` — #4518 (PRD §12.6). The vendored OASF 1.0.0 skills/domains
5+
* taxonomy, loaded from the `oasf/skills-*.tsv` / `oasf/domains-*.tsv` resources. OASF skills/domains
6+
* are not free text: each is a
7+
* `{name, id}` where `id` is the taxonomy uid. The uids are explicitly assigned per node (top-level
8+
* categories are multiples of 100; deeper levels follow a per-level breadcrumb), so this is a lookup
9+
* table, not a computed formula — `path -> uid`, resolved offline and reproducibly.
10+
*
11+
* Slice 1 seeds the confirmed core of the tree; slice 2 vendors the complete `agntcy/oasf` schema and
12+
* adds a build-time cross-check against `schema.oasf.outshift.com`. Unknown paths resolve to `null`,
13+
* which `toOasfRecord` treats as "free-form skill" (omitted from the OASF record with a warning).
14+
*/
15+
object OasfTaxonomy {
16+
const val SCHEMA_VERSION: String = "1.0.0"
17+
18+
private val skills: Map<String, Int> = loadTsv("/oasf/skills-1.0.0.tsv")
19+
private val domains: Map<String, Int> = loadTsv("/oasf/domains-1.0.0.tsv")
20+
21+
/** Resolve an OASF skill path (e.g. `"agent_orchestration/multi_agent_planning"`) to its uid, or null. */
22+
fun skillUid(path: String): Int? = skills[path.trim().trim('/')]
23+
24+
/** Resolve an OASF domain path to its uid, or null. */
25+
fun domainUid(path: String): Int? = domains[path.trim().trim('/')]
26+
27+
private fun loadTsv(resource: String): Map<String, Int> {
28+
val text = OasfTaxonomy::class.java.getResourceAsStream(resource)?.bufferedReader()?.use { it.readText() }
29+
?: error("OASF taxonomy resource missing: $resource")
30+
val map = LinkedHashMap<String, Int>()
31+
text.lineSequence().forEach { raw ->
32+
val line = raw.trim()
33+
if (line.isEmpty() || line.startsWith("#")) return@forEach
34+
val tab = line.indexOf('\t')
35+
require(tab > 0) { "Malformed OASF taxonomy line in $resource: \"$raw\" (expected <path>\\t<uid>)" }
36+
val path = line.substring(0, tab).trim()
37+
val uid = line.substring(tab + 1).trim().toIntOrNull()
38+
?: error("Non-numeric uid in $resource: \"$raw\"")
39+
require(path !in map) { "Duplicate OASF path in $resource: \"$path\"" }
40+
map[path] = uid
41+
}
42+
return map
43+
}
44+
}

src/main/kotlin/agents_engine/core/AgentJson.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package agents_engine.core
22

3+
import agents_engine.agntcy.OasfLocator
34
import agents_engine.mcp.McpJson
45

56
/**
@@ -10,12 +11,24 @@ import agents_engine.mcp.McpJson
1011
* description of the agent itself.
1112
*
1213
* Keys are emitted in a fixed order, so the same agent always serializes byte-identically.
14+
*
15+
* #4518 (PRD §12.6) — optional provenance fields ([authors], [createdAt], [locators]) are shared with
16+
* the OASF record (`toOasfRecord`); they are additive and omitted when not supplied, so existing
17+
* callers serialize byte-identically.
1318
*/
14-
fun Agent<*, *>.toAgentJson(version: String? = null, description: String? = null): String {
19+
fun Agent<*, *>.toAgentJson(
20+
version: String? = null,
21+
description: String? = null,
22+
authors: List<String> = emptyList(),
23+
createdAt: String? = null,
24+
locators: List<OasfLocator> = emptyList(),
25+
): String {
1526
val metadata = LinkedHashMap<String, Any?>()
1627
metadata["name"] = name
1728
if (version != null) metadata["version"] = version
1829
if (description != null) metadata["description"] = description
30+
if (authors.isNotEmpty()) metadata["authors"] = authors
31+
if (createdAt != null) metadata["createdAt"] = createdAt
1932

2033
val skillDocs = skills.values
2134
.sortedBy { it.name }
@@ -49,6 +62,9 @@ fun Agent<*, *>.toAgentJson(version: String? = null, description: String? = null
4962
"tools" to toolDocs,
5063
"capabilities" to linkedMapOf<String, Any?>("streaming" to true),
5164
)
65+
if (locators.isNotEmpty()) {
66+
spec["locators"] = locators.map { linkedMapOf<String, Any?>("type" to it.type, "urls" to it.urls) }
67+
}
5268

5369
val doc = linkedMapOf<String, Any?>(
5470
"apiVersion" to AGENT_JSON_API_VERSION,

src/main/kotlin/agents_engine/core/Skill.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,21 @@ class Skill<IN, OUT>(
191191
useMemory = true
192192
}
193193

194+
/**
195+
* #4518 (PRD §12.6) — opt-in OASF taxonomy classification. [path] is the slash-delimited
196+
* OASF skill path (e.g. `"agent_orchestration/multi_agent_planning"`); `toOasfRecord()`
197+
* resolves it to the taxonomy uid via [agents_engine.agntcy.OasfTaxonomy]. Skills without
198+
* an `.oasf(...)` are free-form and are omitted from the OASF record's `skills[]` (OASF
199+
* skills are taxonomy entries, not free text) — with a validation warning.
200+
*/
201+
var oasfPath: String? = null
202+
private set
203+
204+
fun oasf(path: String) {
205+
checkNotFrozen()
206+
oasfPath = path
207+
}
208+
194209
fun execute(input: IN): OUT {
195210
val impl = checkNotNull(implementation) {
196211
"Skill \"$name\" has no implementation. Add implementedBy { } block."
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
description: Source-file knowledge for agents_engine/agntcy/OasfRecord.kt + OasfTaxonomy.kt + OasfLocator.kt — OASF 1.0.0 record export (#4518, PRD §12.6), AGNTCY's content-addressed discovery metadata. Agent<*,*>.toOasfRecord(version, authors, locators, domains, description, createdAt, annotations) is the third discovery exporter beside A2A toAgentCard() and native agent.json. Skills become OASF skills[] only when annotated with .oasf("path"); the vendored OasfTaxonomy resolves path -> uid (lookup, not formula). Deterministic/byte-stable; createdAt/authors/locators caller-supplied (no hidden now()). Call when the IDE LLM reasons about exporting an agent into the AGNTCY directory.
3+
---
4+
5+
# `agents_engine/agntcy/OasfRecord.kt` — OASF 1.0.0 record export (#4518)
6+
7+
The **third discovery exporter** over the native typed agent, beside `A2AServer`/`toAgentCard()` (§12.5)
8+
and `toAgentJson()` (§12.2). AGNTCY's [OASF](https://github.com/agntcy/oasf) record is the discovery
9+
metadata that the DIR directory (a later subtask of epic #4517) stores content-addressed. The native
10+
agent stays the source of truth; this is a projection, exactly parallel to `toAgentCard()`.
11+
12+
```kotlin
13+
val record: String = agent.toOasfRecord(
14+
version = "1.2.0",
15+
authors = listOf("Ada Lovelace <ada@example.com>"),
16+
locators = listOf(OasfLocator("source_code", listOf("https://example.com/agent"))),
17+
createdAt = "2026-06-15T00:00:00Z", // RFC3339, caller-supplied — see Determinism
18+
)
19+
```
20+
21+
## Skills are taxonomy entries, not free text
22+
23+
Only skills annotated with `.oasf("agent_orchestration/multi_agent_planning")` (the `Skill.oasf(path)`
24+
mutator, `core/Skill.kt`) become OASF `skills[]`. Each path resolves to its uid via `OasfTaxonomy`.
25+
Un-annotated skills — and annotated paths absent from the vendored taxonomy — are **omitted** with a
26+
`java.util.logging` warning (they remain in `agent.json`, which carries free-form skills). This is why
27+
an agent with no `.oasf(...)` annotations exports an empty `skills[]`.
28+
29+
## OasfTaxonomy — vendored lookup, not a formula
30+
31+
OASF uids are **explicitly assigned per node**: top-level categories are multiples of 100, but level-2
32+
is `category + n` while level-3 is `level2*100 + nn` — there is *no* single formula. So `OasfTaxonomy`
33+
is a `path -> uid` lookup loaded from `resources/oasf/skills-1.0.0.tsv` (+ `domains-1.0.0.tsv`).
34+
- **Slice 1 (#4518, this change):** seeds the confirmed core of the skills tree; domains TSV is unseeded
35+
(records emit an empty `domains[]`).
36+
- **Slice 2 (follow-up in #4518):** vendor the complete `agntcy/oasf` skills+domains trees and add a
37+
build-time cross-check against `schema.oasf.outshift.com/api/skills`.
38+
39+
## Determinism
40+
41+
Fixed key order; same inputs → byte-identical JSON (like `toAgentJson`). Wall-clock and authorship
42+
(`createdAt`, `authors`, `locators`) are **caller-supplied**, never sampled — no hidden `now()`, so the
43+
record is reproducible and CI-stable. Record key order: `name, version, schema_version, description?,
44+
authors, created_at?, skills, domains, locators, modules, annotations`.
45+
46+
## Shared provenance with agent.json
47+
48+
`toAgentJson(..., authors, createdAt, locators)` gained the same optional provenance fields (additive,
49+
omitted when not supplied → existing callers stay byte-identical): `metadata.authors`,
50+
`metadata.createdAt`, `spec.locators`. `OasfLocator` (its own file for the one-type-per-file guard,
51+
#3199) is the shared `{type, urls}` value.
52+
53+
## Out of scope (deferred, PRD §12.6)
54+
55+
Record **signing** (Sigstore/cosign over OCI) is external to the record JSON. OASF **import/validate**,
56+
the **DIR** gRPC client, and **Identity-verify** are the other subtasks of epic #4517.
57+
58+
## Related files
59+
60+
- `core/AgentJson.kt` — sibling exporter; shares the provenance fields and `OasfLocator`.
61+
- `a2a/A2AAgentCard.kt` — the structural sibling discovery exporter (`toAgentCard()`).
62+
- `core/Skill.kt``oasf(path)` annotation + `oasfPath`.
63+
- `resources/oasf/skills-1.0.0.tsv` — the vendored taxonomy.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# OASF 1.0.0 domains taxonomy — vendored path -> uid (slash-delimited machine names).
2+
# Source: schema.oasf.outshift.com/api/domains (cross-check against github.com/agntcy/oasf).
3+
# Slice 1 (#4518) leaves this unseeded — toOasfRecord emits an empty domains[] until slice 2
4+
# vendors the complete domains tree. `.oasf(...)`-style domain annotations resolve here when present.
5+
# Format: <slash/path>\t<uid> ('#' comments and blank lines ignored)

0 commit comments

Comments
 (0)