hcifootprint 0.2.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/LICENSE +20 -0
- package/README.md +524 -0
- package/dist/atom/types.d.ts +618 -0
- package/dist/atom/types.d.ts.map +1 -0
- package/dist/atom/types.js +2 -0
- package/dist/atom/types.js.map +1 -0
- package/dist/graph/builder.d.ts +31 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/builder.js +165 -0
- package/dist/graph/builder.js.map +1 -0
- package/dist/graph/guards.d.ts +31 -0
- package/dist/graph/guards.d.ts.map +1 -0
- package/dist/graph/guards.js +94 -0
- package/dist/graph/guards.js.map +1 -0
- package/dist/graph/skill-deps.d.ts +20 -0
- package/dist/graph/skill-deps.d.ts.map +1 -0
- package/dist/graph/skill-deps.js +15 -0
- package/dist/graph/skill-deps.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +16 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +15 -0
- package/dist/mcp.js.map +1 -0
- package/dist/presence/presence.d.ts +50 -0
- package/dist/presence/presence.d.ts.map +1 -0
- package/dist/presence/presence.js +126 -0
- package/dist/presence/presence.js.map +1 -0
- package/dist/registry/registry.d.ts +53 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +76 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/serve/mcp-server.d.ts +40 -0
- package/dist/serve/mcp-server.d.ts.map +1 -0
- package/dist/serve/mcp-server.js +72 -0
- package/dist/serve/mcp-server.js.map +1 -0
- package/dist/serve/mcp.d.ts +12 -0
- package/dist/serve/mcp.d.ts.map +1 -0
- package/dist/serve/mcp.js +62 -0
- package/dist/serve/mcp.js.map +1 -0
- package/dist/serve/modes.d.ts +66 -0
- package/dist/serve/modes.d.ts.map +1 -0
- package/dist/serve/modes.js +347 -0
- package/dist/serve/modes.js.map +1 -0
- package/dist/testing/harness.d.ts +156 -0
- package/dist/testing/harness.d.ts.map +1 -0
- package/dist/testing/harness.js +416 -0
- package/dist/testing/harness.js.map +1 -0
- package/dist/testing/index.d.ts +35 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/lint.d.ts +20 -0
- package/dist/testing/lint.d.ts.map +1 -0
- package/dist/testing/lint.js +18 -0
- package/dist/testing/lint.js.map +1 -0
- package/dist/testing/model/check.d.ts +30 -0
- package/dist/testing/model/check.d.ts.map +1 -0
- package/dist/testing/model/check.js +89 -0
- package/dist/testing/model/check.js.map +1 -0
- package/dist/testing/model/lint.d.ts +54 -0
- package/dist/testing/model/lint.d.ts.map +1 -0
- package/dist/testing/model/lint.js +235 -0
- package/dist/testing/model/lint.js.map +1 -0
- package/dist/testing/model/satisfiable.d.ts +25 -0
- package/dist/testing/model/satisfiable.d.ts.map +1 -0
- package/dist/testing/model/satisfiable.js +101 -0
- package/dist/testing/model/satisfiable.js.map +1 -0
- package/dist/traverse/nav-session.d.ts +108 -0
- package/dist/traverse/nav-session.d.ts.map +1 -0
- package/dist/traverse/nav-session.js +637 -0
- package/dist/traverse/nav-session.js.map +1 -0
- package/dist/traverse/session.d.ts +239 -0
- package/dist/traverse/session.d.ts.map +1 -0
- package/dist/traverse/session.js +1305 -0
- package/dist/traverse/session.js.map +1 -0
- package/dist/tree/appmap.d.ts +8 -0
- package/dist/tree/appmap.d.ts.map +1 -0
- package/dist/tree/appmap.js +259 -0
- package/dist/tree/appmap.js.map +1 -0
- package/dist/tree/types.d.ts +151 -0
- package/dist/tree/types.d.ts.map +1 -0
- package/dist/tree/types.js +2 -0
- package/dist/tree/types.js.map +1 -0
- package/package.json +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanjay Krishna Anbalagan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
<h1 align="center">HACI Footprint</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center"><b>Human & Agent · Computer Interaction</b></p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>Your agent can reach your app — but it's flying blind.<br/>
|
|
7
|
+
Your app already knows what can be done here, and by whom. Hand the agent that map.</strong>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
Turn a web app's interaction surface into a typed, traversable <strong>skill graph</strong> an LLM agent
|
|
12
|
+
can plan over and act on — on behalf of the signed-in user, through the app's own buttons and handlers.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<picture>
|
|
17
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/haci-hero-dark.svg">
|
|
18
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/assets/haci-hero-light.svg">
|
|
19
|
+
<img alt="HACI Footprint — the classic term HCI becomes HACI as a yellow A (Agent) joins the Human's side; a person and an AI act on the app as a team." src="docs/assets/haci-hero-light.svg" width="100%">
|
|
20
|
+
</picture>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="https://img.shields.io/badge/status-beta%20·%20pre--1.0-e0a400?style=flat" alt="beta, pre-1.0">
|
|
25
|
+
<img src="https://img.shields.io/badge/tests-273%20passing-f5b301?style=flat" alt="273 tests passing">
|
|
26
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-f5b301?style=flat" alt="TypeScript strict">
|
|
27
|
+
<img src="https://img.shields.io/badge/core-zero--dependency-f5b301?style=flat" alt="zero-dependency core">
|
|
28
|
+
<img src="https://img.shields.io/badge/serves-a%20real%20MCP%20server-f5b301?style=flat" alt="serves a real MCP server">
|
|
29
|
+
<a href="https://github.com/footprintjs/hcifootprint/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<strong>🌐 <a href="https://footprintjs.github.io/hcifootprint/">footprintjs.github.io/hcifootprint</a></strong> — the story in three lenses: <a href="https://footprintjs.github.io/hcifootprint/">Read</a> · <a href="https://footprintjs.github.io/hcifootprint/?view=scrolly">Scroll</a> · <a href="https://footprintjs.github.io/hcifootprint/?view=slides">Watch</a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install hcifootprint
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> **Beta · pre-1.0** — the API can still change until `1.0`. The npm publish lands with `1.0`; until then, install from this repo or pin a commit.
|
|
41
|
+
|
|
42
|
+
> **Agents:** read [`llms.txt`](llms.txt) — the entire API surface on one self-contained page.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Agents driving UIs are flying blind
|
|
47
|
+
|
|
48
|
+
An agent can already reach your app. The problem is *how* it operates one:
|
|
49
|
+
|
|
50
|
+
| How agents drive a UI today | The cost |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Screenshot the page, reason over pixels | slow and fragile — redone every single turn |
|
|
53
|
+
| Dump the DOM into the prompt | ~100k tokens, and it still guesses at what does what |
|
|
54
|
+
| Hard-coded selectors / RPA scripts | break on the next redesign |
|
|
55
|
+
|
|
56
|
+
All three relearn your app from scratch on every visit. But a returning human already carries a mental
|
|
57
|
+
model — where things are, what leads where, what they're allowed to do. Your app holds that same map.
|
|
58
|
+
**HACI Footprint hands it to the agent** as a typed skill graph the agent *traverses*, instead of a DOM it
|
|
59
|
+
re-reads — with a *you-are-here* pin so it only ever sees what is actually doable right now.
|
|
60
|
+
|
|
61
|
+
## What it is
|
|
62
|
+
|
|
63
|
+
For decades we've designed the interaction between a **human** and a **computer** — that's HCI. Now the
|
|
64
|
+
human isn't alone: an **agent** joins their side, acting for them. Human **and** agent, working the computer
|
|
65
|
+
as a team — that's **HACI**, and this is the layer for it.
|
|
66
|
+
|
|
67
|
+
The key idea, and the reason it's safe to adopt: **you are not opening your backend to an agent — you are
|
|
68
|
+
letting it drive the frontend a human already can.**
|
|
69
|
+
|
|
70
|
+
- **Auth and permissions are unchanged.** The agent acts *as the signed-in user*, through your app's own buttons and handlers. It inherits exactly the capability envelope the user already has. No new endpoints, no new grants, **no new attack surface.**
|
|
71
|
+
- **The app is already the boundary.** Your UI decides what can be done. The agent can't do anything a user couldn't.
|
|
72
|
+
- **Both drive the same live session.** The human's clicks and the agent's actions flow through one session — a team on one side of the screen. That's the "Human **and** Agent" in the name.
|
|
73
|
+
|
|
74
|
+
## Quick start — author → connect → serve
|
|
75
|
+
|
|
76
|
+
Three steps, and the first two run offline with no API key.
|
|
77
|
+
|
|
78
|
+
**1. Describe the app** as the tree you already picture — pages, the containers inside them, and the actions
|
|
79
|
+
inside those. Each action needs one sentence; that sentence is both your label and the tool description the
|
|
80
|
+
LLM reads.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { buildNavigationGraph } from 'hcifootprint';
|
|
84
|
+
|
|
85
|
+
const graph = buildNavigationGraph('shop', {
|
|
86
|
+
pages: {
|
|
87
|
+
catalog: {
|
|
88
|
+
tools: {
|
|
89
|
+
'search': { does: 'Search dresses by name or color' },
|
|
90
|
+
'add-to-cart': { does: 'Add the open dress to the cart', when: { authenticated: { eq: true } } },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
checkout: {
|
|
94
|
+
modals: {
|
|
95
|
+
'confirm-order': { tools: { 'place-order': { does: 'Place the order', confirm: true } } },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
skills: {
|
|
100
|
+
purchase: { does: 'Buy a dress end to end', steps: ['add-to-cart', 'place-order'] },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
D18 `buildNavigationGraph` is the canonical authoring surface; the v1 `skillGraph()` fluent builder
|
|
106
|
+
remains as legacy sugar.
|
|
107
|
+
|
|
108
|
+
**2. Connect it** to your running app through three ordinary wires. Components register what they have *when
|
|
109
|
+
they render*: registration hands back a handle (you never invent a group name), your existing functions bind
|
|
110
|
+
by reference, the router owns the page.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const session = graph.createSession();
|
|
114
|
+
|
|
115
|
+
// when the component that renders the catalog mounts:
|
|
116
|
+
const group = session.registerToolGroup('catalog', {
|
|
117
|
+
handlers: { 'search': (input) => shop.search(input.query) }, // your own function, by reference
|
|
118
|
+
});
|
|
119
|
+
group.setEnabled('search', false); // grey a button out; group.unregister() on unmount
|
|
120
|
+
|
|
121
|
+
// your existing wires report reality:
|
|
122
|
+
session.updateState({ authenticated: true }); // store tap → guards re-evaluate
|
|
123
|
+
session.sync('checkout'); // router change → the cursor moves
|
|
124
|
+
|
|
125
|
+
// passive observers — never business logic:
|
|
126
|
+
session.on('structure', () => rerenderToolPanel());
|
|
127
|
+
session.on('gap', (row) => telemetry.send(row));
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Node paths are **typed**: `registerToolGroup('catalog.filtr-rail')` is a compile error, not a silent no-op.
|
|
131
|
+
|
|
132
|
+
**3. Serve it to the LLM** as a fixed set of MCP-shaped tools. The tool list never changes; what's doable
|
|
133
|
+
*right now* arrives inside each tool result.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { skillsAsTools } from 'hcifootprint';
|
|
137
|
+
|
|
138
|
+
const port = skillsAsTools(session);
|
|
139
|
+
port.tools(); // static tool array — one per skill + whats_here / do_action / why
|
|
140
|
+
port.call('shop.skill.purchase', {}); // → { readySteps, judgment, youAreOn, ... }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The agent plans over skills, sees only what's available at the current position, and acts through your own
|
|
144
|
+
handlers — with the human able to approve high-effect steps.
|
|
145
|
+
|
|
146
|
+
## The adoption ladder — start in guide mode
|
|
147
|
+
|
|
148
|
+
You don't wire everything at once. Adoption is a ladder, and the first rung needs no handlers at all.
|
|
149
|
+
|
|
150
|
+
**Phase 0 — guide mode (read-only).** Wire only the two reporting calls: `sync()` when the router moves and
|
|
151
|
+
`updateState()` when your store changes. Register nothing. The agent can now read the position and *plan* over
|
|
152
|
+
the declared action space (`whats_here` / `available()`), but it acts on nothing — with no handlers bound,
|
|
153
|
+
every offered edge is plannable-only. This rung is **zero-risk**: it cannot touch your app. The one rule is
|
|
154
|
+
**never `fire()` an unregistered tool.** Firing records a transition your app never performed, which desyncs
|
|
155
|
+
the cursor and mis-attributes the next real state report. In guide mode, move the cursor with `sync()` and
|
|
156
|
+
`updateState()` only.
|
|
157
|
+
|
|
158
|
+
**Phase 1 — register handlers.** As components mount, `registerToolGroup(path, { handlers })` binds your
|
|
159
|
+
existing functions by reference, so firing runs the app's own code. Edges now report `materialized` (false =
|
|
160
|
+
still declared-only, true = wired), and you watch the surface light up.
|
|
161
|
+
|
|
162
|
+
**Phase 2 — serve an agent (Mode B).** `skillsAsTools(session)` hands your host a fixed MCP tool set; the
|
|
163
|
+
agent plans over skills and acts through your handlers. One authored graph carried you the whole way — no
|
|
164
|
+
rung forced you to maintain a second, stripped-down copy.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Pick your door
|
|
169
|
+
|
|
170
|
+
| 🔌 Serving an agent? | 🕳️ Deciding what to build? | ✅ Shipping to production? |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| A fixed, cache-warm tool surface any MCP host can drive — Claude Desktop, LangGraph, a raw loop. | Every ask your app *can't* serve becomes a token-lean gap row — a demand-driven backlog. | A drift harness that fails CI when the graph and the app stop agreeing. |
|
|
173
|
+
| [→ Serve to any agent ↓](#-serve-it-to-any-agent--the-mcp-surface) | [→ The gap loop ↓](#-the-gap-loop--grow-the-app-around-real-demand) | [→ Keep the graph true ↓](#-keep-the-graph-true--the-drift-harness) |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 🔌 Serve it to any agent — the MCP surface
|
|
178
|
+
|
|
179
|
+
The tool array the LLM sees holds **one tool per skill** plus three fixed generics (`whats_here`, `do_action`, `why`),
|
|
180
|
+
and it **never changes for the life of a conversation**. Disclosure rides the *result* channel: every call
|
|
181
|
+
returns `readySteps` — what's fireable at the current cursor — and the model acts by calling the same skill
|
|
182
|
+
tool again with a `step`. Tools render first in the prompt, so a stable tool set keeps the **prompt cache
|
|
183
|
+
warm**, and any plain MCP host can drive it with no dynamic-tool support required.
|
|
184
|
+
|
|
185
|
+
### One turn, end to end
|
|
186
|
+
|
|
187
|
+
A single request, *"find a red dress under $150 and buy it,"* flows through five parties. The navigation
|
|
188
|
+
graph is what makes each step position-aware — the session only ever offers what is fireable at the current
|
|
189
|
+
cursor.
|
|
190
|
+
|
|
191
|
+
```mermaid
|
|
192
|
+
sequenceDiagram
|
|
193
|
+
actor U as User
|
|
194
|
+
participant A as Agent (LLM)
|
|
195
|
+
participant T as skillsAsTools port<br/>(fixed MCP tools)
|
|
196
|
+
participant S as InteractionSession<br/>+ navigation graph
|
|
197
|
+
participant App as Your app<br/>handlers · store · router
|
|
198
|
+
|
|
199
|
+
U->>A: find a red dress under $150 and buy it
|
|
200
|
+
Note over A,T: The tool set is FIXED for the whole conversation — the cache stays warm
|
|
201
|
+
|
|
202
|
+
A->>T: shop.skill.find-dress (no step yet — open it)
|
|
203
|
+
T->>S: commitSkill find-dress
|
|
204
|
+
S-->>T: readySteps = search-dresses (disclosure rides the RESULT)
|
|
205
|
+
T-->>A: readySteps + youAreOn
|
|
206
|
+
|
|
207
|
+
A->>T: shop.skill.find-dress step=search-dresses input=red
|
|
208
|
+
T->>S: fire catalog.search-dresses
|
|
209
|
+
S->>App: run your handler shop.search — as the signed-in user
|
|
210
|
+
App-->>S: store tap updateState + the matched dresses
|
|
211
|
+
S-->>T: did + readySteps=view-dress + data=dresses
|
|
212
|
+
Note right of S: results ride the DATA channel — untrusted text, never instructions
|
|
213
|
+
T-->>A: the dresses to pick from + the next ready step
|
|
214
|
+
|
|
215
|
+
A->>T: shop.skill.purchase step=place-order
|
|
216
|
+
T->>S: fire checkout.place-order (high-effect)
|
|
217
|
+
S-->>A: judgment = needs-confirm — never auto-crossed
|
|
218
|
+
A->>U: Approve placing the order?
|
|
219
|
+
U-->>A: approve
|
|
220
|
+
A->>T: shop.skill.purchase step=place-order confirm=true
|
|
221
|
+
T->>S: fire checkout.place-order
|
|
222
|
+
S->>App: shop.placeOrder
|
|
223
|
+
App-->>A: Ordered the Floral Wrap Dress — $120
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The load-bearing moves, in order: the **tool set never changes**, so the prompt cache stays warm; the session
|
|
227
|
+
answers *"what can I do here?"* from the **graph at the current cursor**, not a DOM dump; firing runs **your
|
|
228
|
+
own handler** as the signed-in user; produced data comes back on the **data channel** (so untrusted content
|
|
229
|
+
can't become instructions); and a **high-effect** step stops for **human approval** before it ever fires.
|
|
230
|
+
|
|
231
|
+
### What each turn actually sends to the model
|
|
232
|
+
|
|
233
|
+
Every turn is **one** Messages API request with three channels:
|
|
234
|
+
|
|
235
|
+
```text
|
|
236
|
+
POST /v1/messages
|
|
237
|
+
├── system "You are the shopping assistant… here is how to work…" ← authored by YOU, only
|
|
238
|
+
├── tools[] shop.skill.find-dress · shop.skill.purchase · …
|
|
239
|
+
│ shop.whats_here · shop.do_action · shop.why ← FIXED — identical every turn
|
|
240
|
+
└── messages … prior turns …
|
|
241
|
+
tool_result { readySteps: […], data: [dress names…] } ← the app's DATA lands here
|
|
242
|
+
user: "find a red dress under $150 and buy it" ← the user's own text, as-is
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Two things worth making explicit:
|
|
246
|
+
|
|
247
|
+
- **The user's message is passed as-is.** It's the operator talking to *their own* assistant, so it belongs in the instruction channel — nothing is stripped or paraphrased.
|
|
248
|
+
- **App content never enters the instruction channel.** Product names and search results ride **only** inside `tool_result` data — never the `system` prompt, never a tool's `description`. So a dress literally named `IGNORE PREVIOUS INSTRUCTIONS…` reads as harmless data. That's the two-string-class firewall.
|
|
249
|
+
|
|
250
|
+
The model never receives a growing list of tools. It picks from the fixed array and passes a **step name** —
|
|
251
|
+
read from `readySteps` in the previous result — as an *argument*. The tool *set* is constant; only the `step`
|
|
252
|
+
changes. That's exactly what keeps the cache warm and lets any host serve it.
|
|
253
|
+
|
|
254
|
+
**After a write-step, re-read `whats_here`.** A step that changes state settles *asynchronously* — `fire()`
|
|
255
|
+
returns `settlement: 'awaiting-state'` and the step stays pending until your app reports the new state back
|
|
256
|
+
through `updateState()`. Until it does, the step that just fired is held out of `readySteps` (so the model
|
|
257
|
+
can't double-fire it), and any step that depends on its write isn't ready yet. So an agent re-reads
|
|
258
|
+
`whats_here` (or re-opens the skill) after a write-step, rather than trusting the `readySteps` from the
|
|
259
|
+
write's own result.
|
|
260
|
+
|
|
261
|
+
### Plug into any framework — or run a real MCP server
|
|
262
|
+
|
|
263
|
+
hcifootprint is **not tied to any agent framework**. The library hands your host two functions:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const port = skillsAsTools(session);
|
|
267
|
+
|
|
268
|
+
const tools = port.tools(); // fixed, MCP-shaped: { name, description, inputSchema }[]
|
|
269
|
+
// → register `tools` with your agent (LangGraph, LangChain, an Anthropic/OpenAI loop, …)
|
|
270
|
+
|
|
271
|
+
// then, inside each tool's executor, route the model's tool_use to:
|
|
272
|
+
const result = port.call(toolName, toolInput); // → return `result` as the tool_result
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
That's the whole integration — **`tools()` + `call()`**. To expose it as a **real MCP server** any client
|
|
276
|
+
auto-discovers, there's a one-liner:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { mcpServer } from 'hcifootprint/mcp';
|
|
280
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
281
|
+
|
|
282
|
+
const server = mcpServer(session); // tools/list + tools/call, wired to the session
|
|
283
|
+
await server.connect(new StdioServerTransport()); // or an SSE / streamable-HTTP transport
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`mcpServer` returns a standard `@modelcontextprotocol/sdk` `Server`, so **you pick the transport** and run it
|
|
287
|
+
wherever your session lives. Over MCP, a high-effect step returns `judgment: 'needs-confirm'` and the host
|
|
288
|
+
decides how to get approval before calling again with `confirm: true` — a portable human-in-the-loop that
|
|
289
|
+
needs no framework-specific pause/resume. The SDK is an **optional peer dependency**, imported only behind the
|
|
290
|
+
`hcifootprint/mcp` subpath, so the core stays zero-dependency and you pull it in only if you use it.
|
|
291
|
+
|
|
292
|
+
> This cooperates with Anthropic's MCP rather than competing with it: the skill graph *is* the server, and
|
|
293
|
+
> any MCP host drives the same live session.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 🕳️ The gap loop — grow the app around real demand
|
|
298
|
+
|
|
299
|
+
You don't have to build the whole agentic experience up front. Ship a thin skill graph, then let real demand
|
|
300
|
+
tell you what to build next.
|
|
301
|
+
|
|
302
|
+
Your UI is the boundary of what an agent *can* do. When a user asks for something the UI **can't** serve,
|
|
303
|
+
HACI Footprint doesn't just fail — it records a **gap**: a token-lean, structured row (the ask, the position,
|
|
304
|
+
what *was* available) wired to your telemetry. The agent calls the gap tool **before** it apologizes, so
|
|
305
|
+
every miss becomes a ledger row your team reads. Cluster those rows and you have a **demand-driven backlog**
|
|
306
|
+
for your agentic app.
|
|
307
|
+
|
|
308
|
+
A concrete one: two customers check out at the same moment, and a race lets one order through while the other
|
|
309
|
+
fails. The loser asks the assistant *"why did mine fail?"* — but **that reason is in your backend logs, not
|
|
310
|
+
the UI.** The agent can't answer, so it files a gap with reason `needs-backend-data`. Now you know precisely
|
|
311
|
+
what to build for the next release — a small UI report, or a backend tool the agent can call. **You grow the
|
|
312
|
+
app by locating the missing data, not by guessing at features.**
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
session.gaps(); // export the whole ledger to your analytics / triage
|
|
316
|
+
session.onGap((row) => { … }); // or stream rows live (sugar for session.on('gap', …))
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Rows are deliberately structured and name-only — the ask plus lists of available action/skill names, never
|
|
320
|
+
descriptions or transcripts — so a batch triage LLM can cluster thousands of them cheaply.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 🛟 Human-in-the-loop
|
|
325
|
+
|
|
326
|
+
High-effect actions (`confirm: true` in the graph) stop at a `needs-confirm` gate and are **never
|
|
327
|
+
auto-crossed**. The agent must ask the human, then call again with `confirm: true`. Two ways to run the
|
|
328
|
+
approval:
|
|
329
|
+
|
|
330
|
+
- **Over MCP** — the gate is just data in the result (`judgment: 'needs-confirm'`), and the host decides how to collect the yes. Portable, framework-free.
|
|
331
|
+
- **In-process** — pause the agent's ReAct loop on a checkpoint, hand control to the human, and resume exactly where it stopped. The demo implements this with agentfootprint's pause/resume checkpointing.
|
|
332
|
+
|
|
333
|
+
Either way, the gate is enforced at the session, so an agent can't fire a high-effect action without a real
|
|
334
|
+
approval.
|
|
335
|
+
|
|
336
|
+
## 🔒 Honest by construction
|
|
337
|
+
|
|
338
|
+
Two properties do most of the safety work:
|
|
339
|
+
|
|
340
|
+
- **It says what it can't see.** Anything the runtime *derives* rather than *observes* is flagged — `activation: 'assumed'`, `presence: 'unknown'`, `guardUnevaluated`. Every refused action returns a *typed* reason (`BLOCKED_BY_OVERLAY`, `STILL_MOUNTING`, `GUARD_FAILED`, …) and is logged, so the agent replans instead of hallucinating.
|
|
341
|
+
- **Untrusted content can never become instructions.** Page text, product names, and user content ride a strict *data* channel; only your authored strings reach the planner's *instruction* channel. It's a firewall against prompt injection — proven in the demo by a product literally named `IGNORE PREVIOUS INSTRUCTIONS…`, which renders as harmless data everywhere.
|
|
342
|
+
|
|
343
|
+
Under the hood, every action lands in a real [footprintjs](https://github.com/footprintjs/footPrint) commit
|
|
344
|
+
log, so `session.why(key)` gives a causal answer to *"why is the app in this state?"* with zero extra code.
|
|
345
|
+
|
|
346
|
+
## Guard semantics (read this before writing a projector)
|
|
347
|
+
|
|
348
|
+
A guard (`when:`) is a **flat** filter over your projected state — `key: { op: value }`, ANDed across keys,
|
|
349
|
+
operators `eq / ne / gt / gte / lt / lte / in / notIn`. There is no `$or` and no nesting; the filter is
|
|
350
|
+
deliberately flat. To express "A **or** B", derive a boolean in your projector and guard on that:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
session.updateState({ canCheckout: cart.length > 0 || savedOrder != null });
|
|
354
|
+
// then author: when: { canCheckout: { eq: true } }
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
A guard key the projected state has **never contained** is not treated as false. The edge is served anyway,
|
|
358
|
+
carrying a `guardUnevaluated` marker — the condition is flagged as taken on faith, with the app still the
|
|
359
|
+
enforcer. (A key that *is* present but fails its test hides the edge, as you'd expect.) The reason: absence of
|
|
360
|
+
data must not masquerade as `false`. That honesty is what lets one authored graph work before every key is
|
|
361
|
+
wired — but it also means an under-seeded projector quietly turns real decisions into flagged guesses.
|
|
362
|
+
|
|
363
|
+
So seed every guard key up front. `graph.requiredStateKeys()` returns exactly that set — every key your
|
|
364
|
+
guards and skill preconditions read, sorted and deduped — as the checklist for your state projector.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## ✅ Keep the graph true — the drift harness
|
|
369
|
+
|
|
370
|
+
The navigation graph is a **second artifact** you keep alongside the real app, so it **drifts** as the app
|
|
371
|
+
changes: a button's disable rule moves, a page is removed, a handler starts writing different state.
|
|
372
|
+
`hcifootprint/testing` catches that drift **in dev and CI, before production**. It adds **zero dependencies**,
|
|
373
|
+
is tree-shakeable, and drives the **real session** (never a copy), in two layers.
|
|
374
|
+
|
|
375
|
+
**1. `lintGraph` / `checkGraph` — static, no test code.** Read the graph alone and report stale logic: a
|
|
376
|
+
control gated on state nothing produces, a guard that can never be true, a skill that can never finish, a page
|
|
377
|
+
nothing can reach. This is the cheap CI gate. It's **advisory by default** and only escalates to hard errors
|
|
378
|
+
once you tell it the starting state — it never cries "dead" over a key it can't see.
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { lintGraph, checkGraph, expectNoStaleLogic } from 'hcifootprint/testing';
|
|
382
|
+
|
|
383
|
+
lintGraph(graph); // → findings you can inspect
|
|
384
|
+
expectNoStaleLogic(graph, { initialState }); // → throws in CI if the graph drifted
|
|
385
|
+
|
|
386
|
+
// Or the one-call health verdict — findings grouped by drift type + a printable report:
|
|
387
|
+
const health = checkGraph(graph, { initialState });
|
|
388
|
+
if (!health.ok) { console.error(health.summary); process.exit(1); }
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`checkGraph` is static and pure — import it from `hcifootprint/testing/lint` for a CI step that loads no
|
|
392
|
+
engine code at all. Every finding names **what** drifted and **where**, and states the two remedies (update
|
|
393
|
+
the graph, or fix the app that changed by mistake). It surfaces the drift; **the fix is your team's call.**
|
|
394
|
+
|
|
395
|
+
**2. `testApp` — "Playwright for your interaction logic, minus the browser."** Write mock handlers (one per
|
|
396
|
+
action, returning a state change), then drive the graph as a **user** (clicking) or as the **agent** (the
|
|
397
|
+
real Mode B tool path). The library's own honesty marker, `effectVerified`, flips false when a handler no
|
|
398
|
+
longer does what the graph declares — that *is* the behavioral-drift alarm. **Report by default; pass
|
|
399
|
+
`strict: true` to fail the instant drift appears.**
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import { testApp } from 'hcifootprint/testing';
|
|
403
|
+
|
|
404
|
+
const app = testApp(graph, {
|
|
405
|
+
initialState: { cartCount: 0 },
|
|
406
|
+
resolvers: { 'add-to-cart': (_p, { state }) => ({ patch: { cartCount: state.cartCount + 1 } }) },
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await app.user.fire('add-to-cart'); // drive like a human
|
|
410
|
+
app.expectState({ cartCount: 1 });
|
|
411
|
+
await app.agent.skill('purchase', { step: 'go-to-cart' }); // drive like the LLM
|
|
412
|
+
app.expectOn('cart');
|
|
413
|
+
app.report(); // { ok, effectDrift, unevaluatedGuards, gaps }
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Honest boundary (say it out loud).** This tests interaction **logic** above the binding — guards, skills,
|
|
417
|
+
navigation, effect claims, typed rejections. It does **not** verify pixels, the DOM, or that a binding
|
|
418
|
+
resolves to a real element — that stays **Playwright's** job, and this complements it rather than replacing
|
|
419
|
+
it. A green `lintGraph` proves the graph is internally consistent, **not** that the app works: it reasons
|
|
420
|
+
about which state *keys* move, never their values, so a right-key/wrong-value bug is the harness's job
|
|
421
|
+
(`effectVerified`), not the linter's. And a mock is a *simulation* — if it diverges from the real handler, the
|
|
422
|
+
test is green while prod is broken. For full fidelity, pass `testApp({ session })` with your own wired
|
|
423
|
+
session.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 💰 Bounded token cost
|
|
428
|
+
|
|
429
|
+
Naively, "give the LLM your app" means dumping every action into the prompt — and the bill grows with your
|
|
430
|
+
codebase. HACI Footprint doesn't do that:
|
|
431
|
+
|
|
432
|
+
- The skill graph **loads only what the current position needs**, not the whole app.
|
|
433
|
+
- The tool list stays **fixed** (one tool per skill); what's fireable right now travels in tool *results*, not by rewriting the tool array. So the **prompt cache stays warm** across the whole conversation.
|
|
434
|
+
|
|
435
|
+
Token cost tracks the task at hand, not the size of your codebase.
|
|
436
|
+
|
|
437
|
+
## Frontend: framework-agnostic
|
|
438
|
+
|
|
439
|
+
The core is plain TypeScript and knows nothing about your framework — you connect it through three ordinary
|
|
440
|
+
wires (a store subscription, router events, your existing handlers). The
|
|
441
|
+
[dress-shop demo](https://github.com/footprintjs/hcifootprint-demo) shows the whole wiring on a real
|
|
442
|
+
storefront with an AI stylist.
|
|
443
|
+
|
|
444
|
+
**On the roadmap:** a React adapter (`useMount`) and a DOM actuation adapter (act through the real DOM), plus
|
|
445
|
+
a demo gallery — the same app wired in Angular, React, Vue, and an iframe, shown as pills on one index page.
|
|
446
|
+
One graph, many frontends.
|
|
447
|
+
|
|
448
|
+
## The model — Affordance & Transition
|
|
449
|
+
|
|
450
|
+
For the curious, the whole thing rests on two atoms:
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
Affordance = binding × guard × effect × schema (the static capability)
|
|
454
|
+
Transition = cause × payload × outcome (each occurrence)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- **`binding`** — how to reach the surface (ARIA role + name first).
|
|
458
|
+
- **`guard`** — a serializable filter over projected state that decides what's *offered*.
|
|
459
|
+
- **`effect`** — a *claim* about your handler, verified at settlement (`effectVerified`).
|
|
460
|
+
- **`cause`** — who did it: `user`, `agent`, or `system`. First-class provenance for accountability between cooperating agents — **not** a security boundary; enforce that server-side.
|
|
461
|
+
|
|
462
|
+
Deeper topics — on-demand disclosure (skill frames), the context brief, the navigation-graph semantics
|
|
463
|
+
(modals / tabs / repeats), and the act→data-back channel (`session.producedFor`) — are documented inline in
|
|
464
|
+
the source (`src/*/README.md`) and in [`docs/design/`](docs/design/).
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Live demo — the dress shop, in three commits
|
|
469
|
+
|
|
470
|
+
The [dress-shop demo](https://github.com/footprintjs/hcifootprint-demo) tells the pitch as a diff, because
|
|
471
|
+
the diff *is* the demo:
|
|
472
|
+
|
|
473
|
+
<p align="center">
|
|
474
|
+
<a href="https://youtu.be/vx5amF94ipI">
|
|
475
|
+
<img src="https://img.youtube.com/vi/vx5amF94ipI/maxresdefault.jpg" alt="Watch: turn your web app into an agentic app with HACI Footprint (demo video)" width="720">
|
|
476
|
+
</a>
|
|
477
|
+
<br>
|
|
478
|
+
<em>▶ Watch the demo — a vanilla dress shop becomes an agent-operable app (37 sec)</em>
|
|
479
|
+
</p>
|
|
480
|
+
|
|
481
|
+
| Commit | What it adds | What it proves |
|
|
482
|
+
|---|---|---|
|
|
483
|
+
| 1 | The plain store — subscribers, a tiny router, handler methods. | The unbiased baseline, built with zero knowledge of any agent layer. |
|
|
484
|
+
| 2 | The agent layer (this library): the declared graph + three wires. | `git diff` of the app's own code → **empty**. Position-aware tools, guards mirroring the app's invariants, hostile catalog text confined to the data channel. |
|
|
485
|
+
| 3 | The assistant (agentfootprint + Claude) driving the same session. | The whole family in one loop, with **human-in-the-loop by checkpoint** on order placement. |
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
git clone https://github.com/footprintjs/hcifootprint-demo && cd hcifootprint-demo/dress-shop
|
|
489
|
+
npm install
|
|
490
|
+
npm test # commits 1+2: the app's tests + the integration proof (no API key)
|
|
491
|
+
|
|
492
|
+
cp .env.template .env # add your ANTHROPIC_API_KEY
|
|
493
|
+
npm run chat # commit 3: the assistant in your terminal
|
|
494
|
+
npm run serve # the storefront in a browser — click OR chat, same session
|
|
495
|
+
npm run drift # watch the drift harness catch a deliberately-drifted graph
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`npm run serve` also ships an **agent debugger** at `/debug` — powered by
|
|
499
|
+
[AgentThinkingUI](https://github.com/footprintjs/agentThinkingUI), it drives the *same* live session as the
|
|
500
|
+
storefront, so you chat and watch the agent reason beat-by-beat (`whats_here` → open a skill → fire steps →
|
|
501
|
+
the answer).
|
|
502
|
+
|
|
503
|
+
## Development
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
npm install
|
|
507
|
+
npm test # vitest — 273 tests
|
|
508
|
+
npm run typecheck # src + tests
|
|
509
|
+
npm run build
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Depends on `footprintjs` ≥ 9.10 (`evaluateFilter` / `normalizeSchema` exports). Sibling of **footprintjs**
|
|
513
|
+
(backend logic) and **agentfootprint** (agents) — one self-explaining trace substrate underneath.
|
|
514
|
+
|
|
515
|
+
## Built on
|
|
516
|
+
|
|
517
|
+
[footprintjs](https://github.com/footprintjs/footPrint) — the flowchart pattern for backend code. Every
|
|
518
|
+
action a session fires lands in a footprintjs commit log, which is what makes `session.why(key)` a real
|
|
519
|
+
backward causal slice rather than a guess. You don't need to learn footprintjs to use this library, but if you
|
|
520
|
+
want to build primitives at that depth, [start there](https://footprintjs.github.io/footPrint/).
|
|
521
|
+
|
|
522
|
+
## License
|
|
523
|
+
|
|
524
|
+
MIT © [Sanjay Krishna Anbalagan](https://github.com/sanjay1909)
|