Natural-language map control (@honua/sdk-js/nl-map-control)
Experimental. Symbols on this subpath are
@experimentaland may change in any minor release before1.0.0.
@honua/sdk-js/nl-map-control turns a natural-language instruction into a
typed, serializable, inspectable plan over the existing
agent-tools surface, and executes only
reviewed plans. It composes three things the SDK already ships:
- agent-tools — the ten bounded JSON-Schema map/runtime tools
(
inspectMap,listSources,setViewport,addLayer,setFilter,selectFeature,runWidgetQuery,explainCapabilityGap, …), - query-planner — the canonical query IR that data operations are expressed in, and
- agent-safety — signed approval envelopes, effect budgets, and receipts.
There is intentionally no opaque NL execution path: per the north-star ADR, an agent prompt must compile to the same typed, inspectable plan that human-authored code uses.
Safety model
- Plan-first.
propose(instruction)returns a serializableNlMapPlan— ordered agent-tool invocations for map operations, with query-planner IR attached to data operations — and never executes anything. The plan is content-addressed:fingerprintis the SHA-256 of its canonical JSON, andexecute()recomputes it, so a plan edited after review is rejected (plan-invalid). - Plans only.
execute()is the single execution path and accepts plans only. Passing raw natural language (or anything that is not ahonua.nl-map-plan) throws a typedplan-requirederror. - Policy-gated read auto-execution. A plan whose every step is
readmay auto-execute whenpolicy.autoExecuteReadOnlyallows it (the default). Set it tofalseto require approval for everything. - Envelopes for effects. Any plan containing a
viewportormutationstep requires a signed agent-safety approval envelope. The envelope is a realagent-safetyapproval: the plan is deterministically projected into anAgentPlanV1, dry-run against a policy, signed by a host-owned signer, and verified (signature, expiry, policy, bindings, and per-step parameter digests) before anything runs. An approval issued for one plan does not verify against another. - Receipts. Every execution — auto or approved — emits an
NlMapPlanReceiptrecording the plan identity, mode, per-step tool status, and outcome. Receipts are content-addressed (receiptDigest) and optionally signed with a host-providedreceiptSigner. - BYO LLM. The model transport is a caller-provided callback
(request: NlCompletionRequest) => Promise<NlCompletionResponse>. The SDK depends on no model-vendor SDK, sends the model only the bounded tool schemas plus the semantic map context, and persists no NL or model output.
API walkthrough
import {
approveNlMapPlan,
createNlMapControl,
nlMapRuntimeBinding,
} from "@honua/sdk-js/nl-map-control";
const nl = createNlMapControl({
// Any HonuaAgentRuntime adapter: a HonuaController, a MapPackage runtime,
// or your own object over a MapLibre map.
tools: { runtime },
// Bring your own model. Any provider, any transport.
llm: async (request) => callMyModel(request),
policy: { autoExecuteReadOnly: true, maxSelfCorrections: 2, actor: "app@example" },
approvalVerifier, // host-owned envelope verifier (agent-safety)
});
// 1. Compile NL into an inspectable plan. Nothing executes here.
const plan = await nl.propose("zoom to downtown and only show open incidents");
console.log(JSON.stringify(plan, null, 2)); // render for human review
// 2. Read-only plans can run directly; effectful plans need an envelope.
if (plan.readOnly) {
const { receipt } = await nl.execute(plan);
} else {
const approval = await approveNlMapPlan({
plan,
actor: "app@example",
approver: "operator@example",
signer, // host-owned envelope signer
bindings: {
map: nlMapRuntimeBinding({ observedAt: new Date().toISOString() }),
incidents: incidentsSourceBinding, // agent-safety source binding
},
issuedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 300_000).toISOString(),
});
const { receipt } = await nl.execute(plan, { approval });
console.log(receipt.outcome, receipt.approvalDigest);
}
The completion contract
NlCompletionRequest carries everything a provider adapter needs: the
system prompt (semantic map context from agent-tools), the user
messages, and the tool schemas in provider-neutral MCP-compatible shape
({ name, description, inputSchema }). Adapters map those onto their
provider's native function-calling API and return
{ toolCalls: [{ name, arguments }] } — arguments may be a parsed object
or a raw JSON string. A provider refusal is returned as { refusal } and
surfaces as a typed refusal error.
Self-correction
When a completion fails validation — an unknown tool, arguments that fail
the JSON schema, an invalid canonical query, or a capability miss — the
layer sends a structured retry (purpose: "self-correct") containing the
previous tool calls and typed issues. Capability misses embed the full
explainCapabilityGap output (supported flag, declared capabilities, and a
suggested action) so the model can re-plan against a source that actually
supports the operation. Retries are bounded (maxSelfCorrections, default
2); exhaustion throws retries-exhausted carrying the final issues.
Deterministic replay
createRecordedNlLlm(exchanges) replays committed request/response
fixtures in order and fails loudly on drift. The SDK's own test lane
(test/nl-map-control.test.ts + test/fixtures/nl-map-control/) replays
recorded completions to byte-identical plans, effects, and receipts against
a mock runtime host; the gallery demo (npm run demo:nl-map-control) uses
the same fixtures so it is deterministic with no API key.
The MCP path
The NL layer's tool surface is published through the same agent-tools
exporters used by @honua/mcp-server, so the capability works in-app and
over the Model Context Protocol:
import {
toNlMapControlMcpToolDefinitions,
toNlMapControlOpenAiToolDefinitions,
} from "@honua/sdk-js/nl-map-control";
// proposeMapPlan + executeMapPlan + the ten agent tools
const mcpTools = toNlMapControlMcpToolDefinitions();
const openAiTools = toNlMapControlOpenAiToolDefinitions();
proposeMapPlan is a read tool (planning only); executeMapPlan is an
action tool that requires opt-in and accepts a serialized plan plus an
optional approval bundle — the same plan-only, envelope-gated contract as
the in-app API. MCP servers hosting these tools keep the identical safety
semantics because they delegate to createNlMapControl.
Errors
All failures are typed HonuaNlMapControlErrors with a code:
| Code | Meaning |
|---|---|
refusal |
The model declined the instruction. Not retried. |
retries-exhausted |
No valid plan within the self-correction budget; issues carries the typed reasons (including explainCapabilityGap output). |
plan-required |
execute() received something other than a proposed plan (for example raw NL). |
plan-invalid |
Plan content does not match its fingerprint, or is structurally invalid. |
approval-required |
The plan has viewport/mutation effects and no envelope was supplied (or read auto-execution is disabled). |
approval-invalid |
Envelope verification failed, or the envelope does not bind to this plan. |
fixture-mismatch |
A recorded-replay LLM saw a request that drifted from the recording. |
invalid-options |
Missing runtime, LLM callback, or source binding. |
Approval issuance itself uses agent-safety and throws
HonuaAgentSafetyError (for example policy-denied when the policy forbids
the plan's effects).
See also
docs/agent-safety.md— the envelope, budget, audit, and receipt contracts this layer builds on.docs/agent-safety-threat-model.md— the threat model for the (stable)agent-tools/agent-safetysurface underneath this layer: envelope forgery, replay, effect-budget bypass, receipt tampering, and plan-fingerprint mismatch, each with its conformance test.examples/nl-map-control/— deterministic MapLibre demo with a fixture LLM, plan/receipt JSON panes, and an approve button.- North-star ADR — why NL must compile to the same plan human code uses.