Quineling A2A integration
The experimental agent SDK adapts the local thought-to-lifeform runtime to Agent2Agent requests. The package pins @a2a-js/sdk 1.3.0; examples here target its native A2A 1.0 wire model. QDL remains experimental. Protocol compatibility does not freeze source formats or imply unrestricted thought compilation.
Runtime operation names and arguments follow packages/agent-sdk/src/types.ts; transport behavior follows src/a2a.ts. The adapter mounts JSON-RPC and HTTP+JSON, supports streaming, and enables JSON-RPC v0.3 compatibility by default. Its unauthenticated server is intended for local use.
The official SDK supplies discovery, clients, request handlers, stores, and transports. Consult the released A2A specification for protocol requirements and the compatibility guide before connecting v0.3 peers.
Discovery and versioning
createA2AApp serves /.well-known/agent-card.json with the SDK agentCardHandler. The card identifies the transport endpoint independently of the discovery URL. A native card has an ordered supportedInterfaces array; its default preferred entry is:
{
"url": "http://127.0.0.1:8049/a2a/jsonrpc",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
The CLI defaults to this local port; no deployment is implied. HTTP+JSON is mounted at /a2a/rest. Agent version is 0.0.0-experimental, separate from interface protocolVersion. The card advertises streaming and disables push notifications; it does not configure an extended authenticated card.
The card has three skills: build covers source preparation and inspection; ranch covers offspring preparation/admission, lineage, annotation and explicit world control; execute covers explicit run and reproduce. Ranch operations never run tasks. Skills describe discovery capabilities; the request's operation selects the runtime action. A2A has no standard execution skillId argument.
After building the package, run node dist/a2a-cli.js from packages/agent-sdk; set QUINELING_A2A_PORT to change its port. The CLI binds to 127.0.0.1. To embed it:
import { createA2AApp } from '@quinelings/agent-sdk/a2a';
const { app, runtime, card } = createA2AApp({
baseUrl: 'http://127.0.0.1:8049',
legacyCompat: false
});
const server = app.listen(8049, '127.0.0.1');
The factory also accepts runtime, taskStore, and requestLimitBytes. Its default JSON body limit is 2 MiB. Supply the advertised listener origin as baseUrl; changing the card alone does not open a listener.
Structured request contract
Send a user message with exactly one JSON data part containing a request; use mediaType: "application/json". A single text part is shorthand for create with that exact thought. Text is passed unchanged to the runtime: empty or whitespace-only thoughts request clarification, and the 16,384-character limit applies before trimming. The integration preserves the runtime argument names:
operation | Fields beyond operation | Runtime response |
parse | thought | Parse status, intent when supported, diagnostics, assumptions, source map |
create | thought, optional options | Creation status and artifact when supported |
compile | typed intent, optional options | Validated artifact; invalid intent raises an error |
inspect | artifactId | Full stored Quineling artifact |
run | artifactId | Fresh source-bound execution record |
reproduce | artifactId, recordId | {artifact, record} for a fresh child execution |
recover | recovery | Artifact validated from source or one genome |
frame | artifactId, finite phase, optional options | Geometry points, normals, owners, ridges, node IDs |
offspringPreview | input | Ready candidate or rejected diagnostics, no child storage |
offspringFrame | input, candidateId, childSourceHash, phase, options? | {candidateId,childSourceHash,frame} from stateless rebuild |
offspringAdmit | input, candidateId, childSourceHash, target, requestId | Explicit admission acknowledgement; no execution record |
lineage | artifactId?, cursor?, limit? | {derivations,nextCursor?} flat session evidence |
annotate | artifactId, intent | Exact graph-matching companion attachment |
worldCreate | worldKey, seed, affinity? | One world snapshot, identical configuration idempotent |
worldInspect | worldId | Detached world snapshot, no social advance |
worldCommand | worldId, expectedRevision, sequence, command | Explicit bounded mutation acknowledgement |
Creation options are {seed?, repeats?}. Frame options are {budget?, crests?}. Recovery accepts exactly one of {source}, {harmonics}, or {colors}. Use the actual types rather than inventing a numeric genome array or transport-specific source format. Imported source is validated as a Quineling program; recovery never invokes arbitrary JavaScript evaluation.
compile.intent uses format: "quineling-intent", a name and thought, typed supplied inputs, ordered operation steps, and declared outputs. See the source intent schema and SDK declarations. Units and ordered ports affect task meaning. An unsupported operation cannot be enabled by adding its name to a remote request. All sixteen dispatch operations are closed records; unknown fields, including nested recipe/style/origin/command fields, reject. Public ranch types are in src/ranch-types.ts; the API reference gives their exact shapes. offspringPreview nests its complete input; frame/admission fields are flat alongside operation. worldCreate has no nested config object and worldCommand has no wrapper around its revision/sequence fields.
Example raw JSON-RPC request to the advertised endpoint:
POST /a2a/jsonrpc HTTP/1.1
Host: 127.0.0.1:8049
Content-Type: application/json
A2A-Version: 1.0
{
"jsonrpc": "2.0",
"id": "create-1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-create-1",
"role": "ROLE_USER",
"parts": [{
"mediaType": "application/json",
"data": {
"operation": "create",
"thought": "[2,3,4] | square | sum | report total"
}
}]
}
}
}
Native SDK objects differ from serialized JSON. Use codecs to supply defaults and flatten protobuf unions:
import { randomUUID } from 'node:crypto';
import { SendMessageRequest } from '@a2a-js/sdk';
import { ClientFactory } from '@a2a-js/sdk/client';
const client = await new ClientFactory().createFromUrl('http://127.0.0.1:8049');
const request = SendMessageRequest.fromJSON({
message: {
messageId: randomUUID(),
role: 'ROLE_USER',
parts: [{
mediaType: 'application/json',
data: { operation: 'create', thought: '[2,3,4] | square | sum | report total' }
}]
}
});
const response = await client.sendMessage(request);
if ('artifacts' in response) {
for (const artifact of response.artifacts) {
for (const part of artifact.parts) {
if (part.content?.$case === 'data') console.log(part.content.value);
}
}
}
Do not JSON.stringify an SDK content: {$case: "data", value: ...} object and post it directly; use SendMessageRequest.toJSON for manual transport. Native wire parts have no kind: "data", user role is not "user", and the RPC method is not message/send.
Structured ranch requests and original-key retries
The native data part remains {operation,...fields} and the result data remains {operation,result}. The ranch skill introduces no separate message wrapper or skillId parameter. Text input continues to mean create; it cannot implicitly choose a recipe, admit offspring, advance a ranch, or execute a task.
// Continue with the connected official client above.
async function sendOperation(data) {
const response = await client.sendMessage(SendMessageRequest.fromJSON({
message:{messageId:randomUUID(),role:'ROLE_USER',
parts:[{mediaType:'application/json',data}]}
}));
if (!('artifacts' in response)) throw new Error('Expected an A2A task');
for (const artifact of response.artifacts) {
for (const part of artifact.parts) {
if (part.content?.$case === 'data') return part.content.value;
}
}
throw new Error(JSON.stringify(response.status));
}
const createdWorld = await sendOperation({operation:'worldCreate',
worldKey:'a2a-garden',seed:23,affinity:'neutral'});
const worldId = createdWorld.result.id;
const inspected = await sendOperation({operation:'worldInspect',worldId});
const snapshot = inspected.result;
const originalCommand = {operation:'worldCommand',worldId,
expectedRevision:snapshot.revision,sequence:snapshot.nextSequence,
command:{kind:'advance',ticks:4}};
const advanced = await sendOperation(originalCommand);
const sameAcknowledgement = await sendOperation(originalCommand);
These two sends use different A2A message/task identifiers while preserving the original complete application command. The retained sequence/payload returns the original acknowledgement without advancing again. New commands use a freshly inspected revision and exact nextSequence. Matching retained replay precedes freshness checks; conflicting/gap/discarded stale sequences refuse. Multi-tick success advances revision once. Admission and annotation can change world revision without consuming a world-command sequence.
WorldCreate accepts key 1..64 characters, uint32 seed, and optional affinity structural/neutral. Omitted affinity means structural. Identical normalized configuration returns the current snapshot; a different configuration conflicts in the same Runtime. Neutral scoring uses distance only; structural scoring adds bounded operation-role overlap, gesture and source diversity. Neither determines typed offspring compatibility. Imported adults start energy 60 with participation disabled. worldInspect and frames are passive; social time requires explicit advance commands 1..4.
The complete offspring input has ordered {artifactId,intentHash} parents, compose/mate/merge/body recipe, uint32 nonce, closed mutation style, and manual or pairing origin. Intent pins are canonical companion SHA256 or null only for absent companions; do not invent hashes. The ranch guide supplies executable parents and pin construction. With a complete manual input:
const prepared = await sendOperation({operation:'offspringPreview',input});
if (prepared.result.status !== 'ready') {
throw new Error(JSON.stringify(prepared.result.diagnostics));
}
const c = prepared.result.candidate;
await sendOperation({operation:'offspringFrame',input,candidateId:c.candidateId,
childSourceHash:c.childSourceHash,phase:0,options:{budget:4000,crests:2}});
const originalAdmission = {operation:'offspringAdmit',input,
candidateId:c.candidateId,childSourceHash:c.childSourceHash,
target:{kind:'library'},requestId:'a2a-manual-birth-1'};
const admitted = await sendOperation(originalAdmission);
const sameBirth = await sendOperation(originalAdmission);
const evidence = await sendOperation({operation:'lineage',
artifactId:admitted.result.artifactId,limit:16});
// Only this separate explicit request evaluates the child's task:
const executed = await sendOperation({operation:'run',artifactId:admitted.result.artifactId});
Preview/frame are stateless and store no child or lineage. Frame and admission rebuild from complete input and verify candidateId and childSourceHash. A semantic preview refusal has result.status:'rejected' in a successfully completed A2A task; completion is not evidence that a candidate is ready or admitted. Frame phases are finite within ±1e9, options budget 4000..24000 and crests 2..4. Mutation overrides require all six integer traits in −1000..1000 and mutation none.
For world birth, choose a recipe explicitly after obtaining a live proposal. Build parents from its ordered artifact/intent pins, pairing origin {kind:'pairing',worldId,proposalId,parentResidents,epochs}, and target {kind:'world',worldId,expectedRevision}. Reciprocal courtship creates only a proposal. Admission rechecks fresh source/intent/epoch pins, participation, adulthood/rest, energy≥50 each, half-open expiry, geometry and resources. It consumes once, charges 30 each, inserts one disabled energy 40 child for 200 nursery ticks, and advances revision once. Pending proposal parents recover during cooldown. Withdrawal/retirement/annotation cancels affected links; re-enabling never revives a proposal. Another admission key cannot reuse a consumed proposal.
After a lost response, send the original complete admission requestId/payload or world sequence/payload, including its old expected revision. A new messageId/taskId is a transport identifier, not an application retry key. Changing revision, recipe, target or identities under a successful key conflicts. Exact successful admission replay returns its saved acknowledgement before rebuild/freshness. There is no A2A message deduplication for run or reproduce; resending them creates fresh execution.
lineage is a session-local flat append-order query, limit 1..32 default 16, with numeric nextCursor. It is distinct from A2A ListTasks and its opaque signed cursor. Source heredity parent hashes assert ancestry; session derivations preserve admitted construction evidence, while verified replay still requires exact parents and companions. A source recovered into a fresh Runtime does not restore omitted thought/units, external lineage or old execution records. Explicit annotate data is {operation:'annotate',artifactId,intent}; only absent/identical exact graph-matching metadata is accepted, and affected world pins/epochs invalidate atomically. The same staging applies to first companion attachment by compile of recovered source. Compound birth and metadata enrichment advance the public world revision once.
Results, identity, and execution
The response is an A2A task carrying a structured artifact whose data is {operation, result}. Each task uses the stable result-container ID quineling-result; a follow-up replaces that container, so a successful result supersedes the earlier clarification payload. Successful requests complete the task. A clarify result interrupts it with TASK_STATE_INPUT_REQUIRED; unsupported or inconsistent rejects it with TASK_STATE_REJECTED. The structured result retains status and diagnostics with no created artifact. Read result.status before treating creation as successful. Typed compile returns the artifact directly as result, rather than a creation-status wrapper.
The A2A result artifact is a protocol container. Its artifactId identifies that container inside the A2A task. The nested Quineling artifact id identifies canonical source using SHA-256. A2A task ID, context ID, and message ID are separate identifiers. Preserve the returned runtime artifact.id for inspect, run, and frame requests, and the returned execution record id for reproduce.recordId.
Thought, typed intent, contract, and source map are companion metadata; they do not change source identity. If the same canonical source is already stored with an intent, submitting a different companion intent raises metadata-conflict rather than overwriting the existing interpretation. Use separate Runtime instances to retain both interpretations. Recovering source into a fresh Runtime restores the program without those omitted companions; recovery into a Runtime that already knows the source returns its retained artifact.
Creation constructs validated source and authored anatomy without executing the program. Inspection and frame sampling are observations. run executes explicitly; reproduce verifies the specific parent record and performs a fresh child execution. A reproduced source must match exactly, while execution records have their own identity and parent lineage. Actions supported by the kernel registry are simulations, not external world actions.
The runtime defaults to at most 128 artifacts and 256 execution records in memory; its constructor allows bounded overrides. A full store rejects new entries with resource-limit; it does not silently evict retained entries. Process restart loses those entries. Artifacts also have a32 MiB aggregate budget including companions/genomes. Ranch stores cap one world/1 MiB snapshot,32 residents/nursery 8/pairs 16/proposals 16, candidates 2 MiB, derivations 32 KiB each/128 entries/4 MiB total, admission receipts 128 without eviction, world receipts 256, events 256, and acknowledgements 4 KiB. Required timers/counters stop at 1e6. Full ledgers refuse new admission without partial charges or artifact enrichment. These are Runtime limits, separate from A2A task history. Source and genome exports can recover a validated program; they do not recover omitted thought provenance or historical execution records. A reproduction request must use a currently retained record associated with that exact artifact/source.
Lifecycle and errors
Malformed JSON and oversized HTTP bodies fail before dispatch (400/413). The executor creates an initial task, then validates message role/content and runtime arguments. Failures at that stage, including unknown runtime artifact/record references, produce TASK_STATE_FAILED with a status message rather than a success artifact. Runtime errors retain their structured {code,message,path} at task.status.message.metadata.quinelingError, including in later GetTask responses. A malformed role/part envelope on an existing interrupted task leaves it TASK_STATE_INPUT_REQUIRED, allowing a corrected follow-up; a valid envelope that fails runtime validation still produces a failed task. Unknown A2A task IDs and terminal cancellation use SDK semantic errors. These identifiers must remain distinct.
The implementation uses AgentExecutor, DefaultRequestHandler, and AgentEvent from @a2a-js/sdk/server. Each invocation publishes an initial task before updates. JSON-RPC mounting uses jsonRpcHandler({requestHandler, userBuilder}); discovery uses agentCardHandler({agentCardProvider}). SDK error classes come from @a2a-js/sdk/errors.
A retained clarification response can be continued with a new message ID and the same task/context. Previous user and agent messages remain in the task history, including unsuccessful message attempts. Send a complete replacement thought or structured request: the executor does not merge partial data or infer omitted arguments. On entering TASK_STATE_INPUT_REQUIRED, it releases active execution state and the SDK event bus; follow-up and cancellation are reconstructed from retained task storage. Streaming uses client.sendMessageStream(request) and yields SDK StreamResponse.payload values (task, statusUpdate, artifactUpdate, or message). The artifact arrives before the terminal/interrupted status. Cancellation can win before synchronous dispatch or while a clarification task remains retained; once bounded synchronous evaluation or admission commit begins, it is not interruptible or reversible by cancellation. Disconnecting does not establish cancellation. Runtime admission stages source, lineage, world, both charges and acknowledgement before one synchronous store swap; this is in-memory atomicity, not crash durability or cross-process serialization. Response serialization or A2A task-storage failure can occur after a Runtime mutation committed. Preserve and retry original application keys as above; the adapter itself has no message deduplication.
The default BoundedTaskStore retains up to 128 tasks and 64 MiB, with a 16 MiB per-task limit. At capacity it evicts eligible terminal or paused tasks in order of their last update. Paused tasks also expire after 15 minutes; expiry is checked lazily on load, save, or list. Active and resuming work is protected within the task's tenant/user scope. Failed capacity admission or cloning leaves existing task entries intact; eviction is committed only after the replacement is prepared and shown to fit. Polling a task does not extend its lifetime. Once an ID is evicted or expires, polling, cancellation, or follow-up returns task-not-found; start a new task with a complete request. Runtime artifact/record stores instead reject new entries at capacity.
To change paused-task retention, supply taskStore: new BoundedTaskStore(128, 64 * 1024 * 1024, {interruptedTtlMs: 30 * 60 * 1000}) to createA2AApp; import BoundedTaskStore from @quinelings/agent-sdk/a2a. The optional now callback supports deterministic expiry tests. Custom task stores own their retention policy.
ListTasks uses an opaque signed cursor bound to the store instance, tenant/user scope, and context/status/timestamp filters. Results are ordered by status timestamp from newest to oldest; immutable admission sequence breaks equal-time ties. The cursor records both values, so eviction of earlier rows or the cursor row does not shift the continuation. Reusing a cursor with changed membership filters or another scope/store is rejected. This is a live listing: status changes can move a task across the cursor, and eviction or expiry can remove it. It does not promise snapshot consistency or exactly-once enumeration during mutation. Start a fresh listing to refresh changed results; totalSize counts the currently retained matches.
statusTimestampAfter compares parsed instants, including timezone offsets, and includes a task at exactly the supplied instant. Missing or invalid stored timestamps are excluded when that filter is present. Inclusive filtering and newest-first ordering follow the A2A 1.0 List Tasks contract; the installed SDK's reference in-memory store uses a stricter timestamp comparison, which this adapter deliberately does not copy.
The factory mounts UserBuilder.noAuthentication and a shared Runtime. Remote service use requires an adapted authenticated mount and principal-scoped runtime access in addition to the task store's tenant/user scope. Runtime IDs are references, not access tokens. Source/thought artifacts may contain supplied data; apply the same authorization to downloads. No credentials belong in source or the agent card.
Interoperability checks before release
Use isolated runtimes and an ephemeral loopback listener. This checklist specifies required coverage; it is not a test result report.
- Discover with the official client; validate interface version, endpoint, modes, skills, and examples against the actual mounted service.
- Create the square/sum example, inspect its source, then run and assert total 29 independently. Reproduce using the returned current record, assert exact source and fresh parent-linked record, then recover separately through each genome representation.
- Exercise every structured operation through transport and compare it with direct
Runtime.dispatch. Frame/inspect/recover must not create execution records. - Preserve clarification/unsupported/inconsistent diagnostics and reject unknown operations, multiple data payloads, malformed intent, non-finite inputs, and conflicting recovery representations before mutation.
- Test polling, history limits, clarification transcript preservation/result replacement, malformed follow-up recovery, terminal cancellation, wrong task/context pairs, missing runtime references, and unsupported push/stream requests. Card claims must agree with observed behavior.
- Submit more clarification requests than the task capacity, then create a valid lifeform. Verify retained clarification follow-up/cancellation, expired or evicted IDs returning task-not-found, and active/resuming tasks surviving cleanup. Check conflicting companion intents preserve the existing artifact.
- Test cursor continuation after eviction, newest-first ordering, inclusive chronological timestamp filters, atomic failed saves, and tenant-scoped active protection.
- Include raw HTTP fixtures to catch accidental v0.3 wire fields. When possible, add an official independent-language client or conformance suite; same-SDK round trips alone do not establish interoperability.
JSON-RPC legacy compatibility defaults to enabled; legacyCompat: false disables it. Discovery and JSON-RPC handlers use the upstream compat layer, and the card advertises a v0.3 JSON-RPC interface. REST remains native A2A 1.0. For a legacy server client, enable upstream compat in card resolution and the selected client transport, and test it separately. Do not send old field names to a native-only service.
On 2026-10-04, native SendMessage envelopes were exercised through an ephemeral loopback service using actual SDK source: all eight ranch operations, the build/ranch/execute discovery skills, stateless frame budget 4000, exact admission/command retries with new transport message IDs, lineage/annotation, and a separate composed child run returning {allocated:12,remaining:8} passed. This supplies same-SDK HTTP payload evidence; it does not establish independent-language interoperability or remote deployment.