@nxisai/agent-harness 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +31 -9
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/tools/action-tools.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.js +7 -5
- package/dist/harness/tools/action-tools.js.map +1 -1
- package/dist/harness/tools/bridge.d.ts.map +1 -1
- package/dist/harness/tools/bridge.js +33 -12
- package/dist/harness/tools/bridge.js.map +1 -1
- package/dist/harness/tools/local-context.d.ts +49 -9
- package/dist/harness/tools/local-context.d.ts.map +1 -1
- package/dist/harness/tools/local-context.js +377 -22
- package/dist/harness/tools/local-context.js.map +1 -1
- package/dist/harness/tools/open-page.d.ts +47 -1
- package/dist/harness/tools/open-page.d.ts.map +1 -1
- package/dist/harness/tools/open-page.js +119 -33
- package/dist/harness/tools/open-page.js.map +1 -1
- package/dist/harness/tools/retry.d.ts +53 -0
- package/dist/harness/tools/retry.d.ts.map +1 -0
- package/dist/harness/tools/retry.js +96 -0
- package/dist/harness/tools/retry.js.map +1 -0
- package/dist/harness/tools/schema.d.ts +18 -0
- package/dist/harness/tools/schema.d.ts.map +1 -1
- package/dist/harness/tools/schema.js +38 -0
- package/dist/harness/tools/schema.js.map +1 -1
- package/dist/harness/tools/site-actions.d.ts.map +1 -1
- package/dist/harness/tools/site-actions.js +3 -2
- package/dist/harness/tools/site-actions.js.map +1 -1
- package/dist/harness/tools/site-nav.d.ts +168 -0
- package/dist/harness/tools/site-nav.d.ts.map +1 -0
- package/dist/harness/tools/site-nav.js +316 -0
- package/dist/harness/tools/site-nav.js.map +1 -0
- package/dist/harness/tools/site-pages.d.ts +9 -4
- package/dist/harness/tools/site-pages.d.ts.map +1 -1
- package/dist/harness/tools/site-pages.js +46 -7
- package/dist/harness/tools/site-pages.js.map +1 -1
- package/dist/harness/tools/source.d.ts +11 -2
- package/dist/harness/tools/source.d.ts.map +1 -1
- package/dist/harness/tools/source.js +55 -15
- package/dist/harness/tools/source.js.map +1 -1
- package/dist/harness/tools/webmcp.d.ts +39 -3
- package/dist/harness/tools/webmcp.d.ts.map +1 -1
- package/dist/harness/tools/webmcp.js +93 -14
- package/dist/harness/tools/webmcp.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +13 -2
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/dist/ui/tailwind.generated.d.ts +1 -1
- package/dist/ui/tailwind.generated.d.ts.map +1 -1
- package/dist/ui/tailwind.generated.js +1 -1
- package/dist/ui/tailwind.generated.js.map +1 -1
- package/package.json +2 -1
- package/src/a2ui/cards.ts +177 -0
- package/src/a2ui/emit.ts +147 -0
- package/src/a2ui/form.ts +117 -0
- package/src/a2ui/host.ts +73 -0
- package/src/a2ui/index.ts +22 -0
- package/src/a2ui/stepper.ts +211 -0
- package/src/a2ui/theme.ts +134 -0
- package/src/a2ui/to-v09.ts +118 -0
- package/src/engine.ts +701 -0
- package/src/harness/harness.ts +956 -0
- package/src/harness/plan.ts +91 -0
- package/src/harness/policy.ts +59 -0
- package/src/harness/renderer.ts +208 -0
- package/src/harness/router/coordinator.ts +220 -0
- package/src/harness/router/rules.ts +65 -0
- package/src/harness/slots.ts +117 -0
- package/src/harness/steps.ts +156 -0
- package/src/harness/tools/action-tools.ts +870 -0
- package/src/harness/tools/bridge.ts +393 -0
- package/src/harness/tools/client.ts +26 -0
- package/src/harness/tools/discovery.ts +55 -0
- package/src/harness/tools/local-context.ts +480 -0
- package/src/harness/tools/open-page.ts +273 -0
- package/src/harness/tools/pending-action.ts +232 -0
- package/src/harness/tools/resume.ts +134 -0
- package/src/harness/tools/retry.ts +117 -0
- package/src/harness/tools/schema.ts +245 -0
- package/src/harness/tools/site-actions.ts +356 -0
- package/src/harness/tools/site-nav.ts +432 -0
- package/src/harness/tools/site-pages.ts +288 -0
- package/src/harness/tools/source.ts +353 -0
- package/src/harness/tools/webmcp.ts +200 -0
- package/src/index.ts +193 -0
- package/src/providers/capability.ts +167 -0
- package/src/providers/proxy.ts +191 -0
- package/src/providers/types.ts +150 -0
- package/src/types.ts +258 -0
- package/src/ui/agent-fullscreen.ts +3984 -0
- package/src/ui/agent.tw.css +49 -0
- package/src/ui/config.ts +138 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/journey.ts +57 -0
- package/src/ui/tailwind.generated.ts +2 -0
- package/src/webmcp-contract.ts +65 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merges every known source of a tool's arguments and reports what is missing.
|
|
3
|
+
*
|
|
4
|
+
* Sources merge by precedence, most authoritative last:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* schema defaults < page context < saved profile < model args
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Model arguments win because they encode what the user has just asked for, so
|
|
11
|
+
* an explicit value in the request overrides a stored preference.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { JsonSchema } from '../types.js';
|
|
17
|
+
|
|
18
|
+
/** One argument of a tool call, with its value and where that value came from. */
|
|
19
|
+
export interface Slot {
|
|
20
|
+
/** Property key in the flat, model-facing schema. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** The property's JSON Schema, used for labels, types and enums. */
|
|
23
|
+
schema: JsonSchema;
|
|
24
|
+
/** Whether the tool's schema lists this property as required. */
|
|
25
|
+
required: boolean;
|
|
26
|
+
/** Which source supplied {@link Slot.value}; `'none'` when nothing did. */
|
|
27
|
+
source: 'model' | 'profile' | 'context' | 'default' | 'none';
|
|
28
|
+
/** The resolved value, or `undefined` when `source` is `'none'`. */
|
|
29
|
+
value: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The outcome of merging every source for one tool call. */
|
|
33
|
+
export interface Resolution {
|
|
34
|
+
/** Every known value, ready to submit when `missing` is empty. */
|
|
35
|
+
values: Record<string, unknown>;
|
|
36
|
+
/** Required slots nobody supplied: the values to ask for. */
|
|
37
|
+
missing: Slot[];
|
|
38
|
+
/**
|
|
39
|
+
* Optional slots left empty. Never asked about on their own; a write
|
|
40
|
+
* confirmation may still surface them as editable extras.
|
|
41
|
+
*/
|
|
42
|
+
optional: Slot[];
|
|
43
|
+
/** Every slot in the schema, resolved or not. */
|
|
44
|
+
all: Slot[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The candidate values to merge, in order of increasing precedence. */
|
|
48
|
+
export interface ResolveSources {
|
|
49
|
+
/** Arguments the model supplied in its tool call. */
|
|
50
|
+
modelArgs?: Record<string, unknown>;
|
|
51
|
+
/** Values saved from earlier turns, such as a name or an email address. */
|
|
52
|
+
profile?: Record<string, unknown>;
|
|
53
|
+
/** Values the host page knows, such as the current record. */
|
|
54
|
+
context?: Record<string, unknown>;
|
|
55
|
+
/** Values a human has typed into a live form. Ranks with `modelArgs`. */
|
|
56
|
+
entered?: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Present and meaningful: not null, not undefined, not whitespace. */
|
|
60
|
+
const isSet = (v: unknown): boolean =>
|
|
61
|
+
v !== undefined && v !== null && !(typeof v === 'string' && v.trim() === '');
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve every property in a schema against the supplied sources.
|
|
65
|
+
*
|
|
66
|
+
* @param schema - The tool's flat input schema.
|
|
67
|
+
* @param sources - Candidate values, merged by the precedence in the module
|
|
68
|
+
* description.
|
|
69
|
+
* @returns The merged values, plus the required and optional slots still empty.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveSlots(schema: JsonSchema, sources: ResolveSources): Resolution {
|
|
72
|
+
const props = schema.properties ?? {};
|
|
73
|
+
const required = new Set(schema.required ?? []);
|
|
74
|
+
|
|
75
|
+
const all: Slot[] = Object.entries(props).map(([name, spec]) => {
|
|
76
|
+
const propSchema = spec ?? {};
|
|
77
|
+
|
|
78
|
+
// Highest precedence first; the first hit wins and records its origin.
|
|
79
|
+
const candidates: [Slot['source'], unknown][] = [
|
|
80
|
+
['model', sources.entered?.[name]],
|
|
81
|
+
['model', sources.modelArgs?.[name]],
|
|
82
|
+
['profile', sources.profile?.[name]],
|
|
83
|
+
['context', sources.context?.[name]],
|
|
84
|
+
['default', propSchema.default],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const [source, value] of candidates) {
|
|
88
|
+
if (isSet(value)) {
|
|
89
|
+
return { name, schema: propSchema, required: required.has(name), source, value };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { name, schema: propSchema, required: required.has(name), source: 'none', value: undefined };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const values: Record<string, unknown> = {};
|
|
96
|
+
for (const slot of all) if (isSet(slot.value)) values[slot.name] = slot.value;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
values,
|
|
100
|
+
missing: all.filter(s => s.required && s.source === 'none'),
|
|
101
|
+
optional: all.filter(s => !s.required && s.source === 'none'),
|
|
102
|
+
all,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The slots to display on a write confirmation: those with a value.
|
|
108
|
+
*
|
|
109
|
+
* Empty optional slots are omitted, since a confirmation shows what is about
|
|
110
|
+
* to be sent rather than what could have been.
|
|
111
|
+
*
|
|
112
|
+
* @param resolution - A resolution with no remaining gaps.
|
|
113
|
+
* @returns Every slot whose value came from a source.
|
|
114
|
+
*/
|
|
115
|
+
export function summarySlots(resolution: Resolution): Slot[] {
|
|
116
|
+
return resolution.all.filter(s => s.source !== 'none');
|
|
117
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the agent is doing, as a short list a surface can show.
|
|
3
|
+
*
|
|
4
|
+
* A chat thread says what was said; it does not say that the page is about to
|
|
5
|
+
* change, or that a form is being filled right now. A copilot surface beside
|
|
6
|
+
* the page needs the second thing, and so does anyone watching a call travel
|
|
7
|
+
* to another page and finish there.
|
|
8
|
+
*
|
|
9
|
+
* The journey outlives the page. A call confirmed on one page is submitted on
|
|
10
|
+
* another, so the steps taken before the navigation are persisted with the
|
|
11
|
+
* call and restored on arrival — otherwise the surface that exists to explain
|
|
12
|
+
* the journey would show only its last step.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The steps a turn can take, in the order they happen.
|
|
19
|
+
*
|
|
20
|
+
* A closed set, so a surface can give each one its own words and ordering
|
|
21
|
+
* rather than printing whatever it is handed. Surfaces should still ignore an
|
|
22
|
+
* id they do not know: this list may grow.
|
|
23
|
+
*/
|
|
24
|
+
export type AgentStepId =
|
|
25
|
+
/** A tool was chosen for the request. */
|
|
26
|
+
| 'choose'
|
|
27
|
+
/** Values are being collected from the user. */
|
|
28
|
+
| 'collect'
|
|
29
|
+
/** Everything is known; a human is being asked to press submit. */
|
|
30
|
+
| 'confirm'
|
|
31
|
+
/** Leaving for the page that can run the action. */
|
|
32
|
+
| 'navigate'
|
|
33
|
+
/** That page is open. */
|
|
34
|
+
| 'arrive'
|
|
35
|
+
/** The form is being filled and submitted. */
|
|
36
|
+
| 'fill'
|
|
37
|
+
/** The turn finished. */
|
|
38
|
+
| 'submit';
|
|
39
|
+
|
|
40
|
+
/** How far a step got. */
|
|
41
|
+
export type AgentStepStatus = 'active' | 'done' | 'failed';
|
|
42
|
+
|
|
43
|
+
/** One step of what the agent is doing. */
|
|
44
|
+
export interface AgentStep {
|
|
45
|
+
/** Which step this is. Recording the same id again updates it in place. */
|
|
46
|
+
id: AgentStepId;
|
|
47
|
+
/**
|
|
48
|
+
* Which journey the step belongs to — one per request that acts.
|
|
49
|
+
*
|
|
50
|
+
* A conversation holds many requests, and a surface that keeps what was
|
|
51
|
+
* done needs to know which of them a step belongs to. Filled in when the
|
|
52
|
+
* step is recorded; carried across a page change with the rest.
|
|
53
|
+
*/
|
|
54
|
+
journey?: string;
|
|
55
|
+
/** What is happening, in the user's words: `Filling the quote form`. */
|
|
56
|
+
label: string;
|
|
57
|
+
/** How far it got. */
|
|
58
|
+
status: AgentStepStatus;
|
|
59
|
+
/** A quieter second line: `name, email, service`. */
|
|
60
|
+
detail?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Listener = (step: AgentStep, all: AgentStep[]) => void;
|
|
64
|
+
|
|
65
|
+
let journey: AgentStep[] = [];
|
|
66
|
+
let journeyId: string | null = null;
|
|
67
|
+
const listeners = new Set<Listener>();
|
|
68
|
+
|
|
69
|
+
const newJourneyId = (): string =>
|
|
70
|
+
'j' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start a new journey, discarding the last one.
|
|
74
|
+
*
|
|
75
|
+
* Called when a turn begins, so a surface does not show the previous
|
|
76
|
+
* request's steps beside this one's.
|
|
77
|
+
*
|
|
78
|
+
* @returns The new journey's id, which every step recorded until the next
|
|
79
|
+
* call carries.
|
|
80
|
+
*/
|
|
81
|
+
export function beginJourney(): string {
|
|
82
|
+
journey = [];
|
|
83
|
+
journeyId = newJourneyId();
|
|
84
|
+
notify(undefined);
|
|
85
|
+
return journeyId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The journey steps are being recorded into, or null before the first. */
|
|
89
|
+
export function currentJourney(): string | null {
|
|
90
|
+
return journeyId;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record a step, or update one already recorded.
|
|
95
|
+
*
|
|
96
|
+
* @param step - The step. An id already present is replaced where it stands,
|
|
97
|
+
* so `active` becoming `done` does not append a second line. The step is
|
|
98
|
+
* stamped with the current journey.
|
|
99
|
+
*/
|
|
100
|
+
export function recordStep(step: AgentStep): void {
|
|
101
|
+
journeyId ??= newJourneyId();
|
|
102
|
+
const stamped = { ...step, journey: journeyId };
|
|
103
|
+
const at = journey.findIndex((s) => s.id === step.id);
|
|
104
|
+
if (at >= 0) journey[at] = { ...journey[at], ...stamped };
|
|
105
|
+
else journey.push(stamped);
|
|
106
|
+
notify(stamped);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The steps so far, oldest first. */
|
|
110
|
+
export function journeySteps(): AgentStep[] {
|
|
111
|
+
return journey.map((s) => ({ ...s }));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Replace the journey with one that was carried here.
|
|
116
|
+
*
|
|
117
|
+
* Used on arrival, where the steps that led to the page change happened
|
|
118
|
+
* before this page existed.
|
|
119
|
+
*/
|
|
120
|
+
export function restoreJourney(steps: AgentStep[] | undefined): void {
|
|
121
|
+
journey = (steps ?? []).map((s) => ({ ...s }));
|
|
122
|
+
// The same journey, continued: steps recorded from here belong with the
|
|
123
|
+
// ones taken on the page before, not to a new request.
|
|
124
|
+
journeyId = journey.find((s) => s.journey)?.journey ?? newJourneyId();
|
|
125
|
+
journey = journey.map((s) => ({ ...s, journey: journeyId! }));
|
|
126
|
+
notify(undefined);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Watch the journey.
|
|
131
|
+
*
|
|
132
|
+
* A renderer receives steps through {@link HarnessRenderer.step} and needs
|
|
133
|
+
* none of this; a surface drawn outside the harness — a side panel the host
|
|
134
|
+
* owns — subscribes here instead.
|
|
135
|
+
*
|
|
136
|
+
* @param listener - Called with the step that changed, and the whole list.
|
|
137
|
+
* @returns Stop listening.
|
|
138
|
+
*/
|
|
139
|
+
export function onJourneyChange(listener: Listener): () => void {
|
|
140
|
+
listeners.add(listener);
|
|
141
|
+
return () => listeners.delete(listener);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function notify(step: AgentStep | undefined): void {
|
|
145
|
+
const all = journeySteps();
|
|
146
|
+
// A restore or a reset has no single step to point at; the list is the news.
|
|
147
|
+
const changed = step ?? all[all.length - 1] ?? { id: 'choose' as const, label: '', status: 'done' as const };
|
|
148
|
+
for (const listener of listeners) {
|
|
149
|
+
try {
|
|
150
|
+
listener(changed, all);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
// One surface throwing must not stop the next from being told.
|
|
153
|
+
console.warn('[nxis-agent] a step listener threw:', err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|