feat(maestro): add elicitation web UI and TUI stub
ElicitationView renders form-mode questions from the agent: single-property string enums as a button group (the common multiple-choice case), general schemas as a form with typed inputs. Wired into TranscriptRow, StructuredPane, and the model helpers. TUI renders elicitation rows as plain text for now. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1e628789fb
commit
5bdedc8d23
@@ -12,6 +12,7 @@ import type {
|
||||
RecentDirs,
|
||||
Session,
|
||||
StructuredApprovalDecision,
|
||||
StructuredElicitationAction,
|
||||
StructuredConfigRequest,
|
||||
StructuredPromptRequest,
|
||||
Uuid,
|
||||
@@ -219,4 +220,20 @@ export async function resolveStructuredApproval(
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve a pending elicitation. Same idempotency as approvals. */
|
||||
export async function resolveStructuredElicitation(
|
||||
id: Uuid,
|
||||
nonce: string,
|
||||
action: StructuredElicitationAction,
|
||||
): Promise<void> {
|
||||
await jsonOrError(
|
||||
await fetch(routes.structuredElicitation(id, nonce), {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(action),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export { eventsWsUrl, structuredWsUrl } from "./routes.js";
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
// An `Elicitation` row. The daemon mints the nonce; this only solicits one
|
||||
// of the three possible actions (accept, decline, cancel).
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type {
|
||||
AcpElicitationProperty,
|
||||
AcpElicitationSchema,
|
||||
StructuredElicitationAction,
|
||||
} from "../../types.js";
|
||||
|
||||
export interface ElicitationViewProps {
|
||||
/** The `CreateElicitationRequest` the row carries — serialized without
|
||||
* its nonce, which never leaves the daemon's live wire. */
|
||||
request: {
|
||||
message?: string;
|
||||
requestedSchema?: AcpElicitationSchema;
|
||||
};
|
||||
/** Whether this is the one live elicitation. */
|
||||
pending: boolean;
|
||||
onDecide: (action: StructuredElicitationAction) => Promise<void>;
|
||||
}
|
||||
|
||||
/** For a single-property string enum schema (the common Claude Code
|
||||
* multiple-choice case), extract the property key and its options. */
|
||||
function singleSelectEnum(
|
||||
schema: AcpElicitationSchema | undefined,
|
||||
): { key: string; options: Array<{ value: string; title: string; description?: string }> } | null {
|
||||
if (!schema) return null;
|
||||
const keys = Object.keys(schema.properties);
|
||||
if (keys.length !== 1) return null;
|
||||
const key = keys[0];
|
||||
const prop = schema.properties[key];
|
||||
if (prop.oneOf && prop.oneOf.length > 0) {
|
||||
return {
|
||||
key,
|
||||
options: prop.oneOf.map((o) => ({
|
||||
value: o.const,
|
||||
title: o.title,
|
||||
description: o.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (prop.enum && prop.enum.length > 0) {
|
||||
return {
|
||||
key,
|
||||
options: prop.enum.map((v) => ({ value: v, title: v })),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ElicitationView({ request, pending, onDecide }: ElicitationViewProps) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [formValues, setFormValues] = useState<Record<string, string | number | boolean>>({});
|
||||
const message = request.message ?? "Input requested";
|
||||
const schema = request.requestedSchema;
|
||||
|
||||
if (!pending) {
|
||||
return <div className="st-row st-row-elicitation st-elicitation-compact">{message}</div>;
|
||||
}
|
||||
|
||||
const decide = (action: StructuredElicitationAction) => {
|
||||
setBusy(true);
|
||||
onDecide(action).catch(() => setBusy(false));
|
||||
};
|
||||
|
||||
const singleSelect = singleSelectEnum(schema);
|
||||
|
||||
// Fast path: single-property string enum → button group (the common
|
||||
// Claude Code multiple-choice question).
|
||||
if (singleSelect) {
|
||||
return (
|
||||
<div className="st-row st-row-elicitation">
|
||||
<div className="st-elicitation">
|
||||
<div className="st-elicitation-title">
|
||||
<span aria-hidden="true">?</span> {message}
|
||||
</div>
|
||||
<div className="st-elicitation-options">
|
||||
{singleSelect.options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
className="act"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
decide({ action: "accept", content: { [singleSelect.key]: opt.value } })
|
||||
}
|
||||
>
|
||||
{opt.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="st-elicitation-acts">
|
||||
<button
|
||||
type="button"
|
||||
className="act deny"
|
||||
disabled={busy}
|
||||
onClick={() => decide({ action: "decline" })}
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// General form: render inputs for each property.
|
||||
const properties = schema?.properties ?? {};
|
||||
const propertyKeys = Object.keys(properties);
|
||||
const requiredSet = new Set(schema?.required ?? []);
|
||||
|
||||
const handleSubmit = () => {
|
||||
decide({ action: "accept", content: formValues });
|
||||
};
|
||||
|
||||
const updateField = (key: string, value: string | number | boolean) => {
|
||||
setFormValues((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="st-row st-row-elicitation">
|
||||
<div className="st-elicitation">
|
||||
<div className="st-elicitation-title">
|
||||
<span aria-hidden="true">?</span> {message}
|
||||
</div>
|
||||
<div className="st-elicitation-form">
|
||||
{propertyKeys.map((key) => {
|
||||
const prop = properties[key];
|
||||
return (
|
||||
<ElicitationField
|
||||
key={key}
|
||||
name={key}
|
||||
prop={prop}
|
||||
required={requiredSet.has(key)}
|
||||
disabled={busy}
|
||||
value={formValues[key]}
|
||||
onChange={(v) => updateField(key, v)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="st-elicitation-acts">
|
||||
<button type="button" className="act primary" disabled={busy} onClick={handleSubmit}>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="act deny"
|
||||
disabled={busy}
|
||||
onClick={() => decide({ action: "decline" })}
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ElicitationField({
|
||||
name,
|
||||
prop,
|
||||
required,
|
||||
disabled,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
name: string;
|
||||
prop: AcpElicitationProperty;
|
||||
required: boolean;
|
||||
disabled: boolean;
|
||||
value: string | number | boolean | undefined;
|
||||
onChange: (value: string | number | boolean) => void;
|
||||
}) {
|
||||
const label = prop.title ?? name;
|
||||
const type = prop.type ?? "string";
|
||||
|
||||
if (type === "boolean") {
|
||||
return (
|
||||
<label className="st-elicitation-field">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={typeof value === "boolean" ? value : (prop.default === true)}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
{label}
|
||||
{prop.description && <span className="st-elicitation-desc">{prop.description}</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "number" || type === "integer") {
|
||||
return (
|
||||
<label className="st-elicitation-field">
|
||||
{label}
|
||||
{required && <span className="st-elicitation-required">*</span>}
|
||||
{prop.description && <span className="st-elicitation-desc">{prop.description}</span>}
|
||||
<input
|
||||
type="number"
|
||||
step={type === "integer" ? 1 : "any"}
|
||||
min={prop.minimum}
|
||||
max={prop.maximum}
|
||||
value={typeof value === "number" ? value : ""}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(type === "integer" ? parseInt(e.target.value) : parseFloat(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: string
|
||||
return (
|
||||
<label className="st-elicitation-field">
|
||||
{label}
|
||||
{required && <span className="st-elicitation-required">*</span>}
|
||||
{prop.description && <span className="st-elicitation-desc">{prop.description}</span>}
|
||||
<input
|
||||
type="text"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
disabled={disabled}
|
||||
placeholder={typeof prop.default === "string" ? prop.default : undefined}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import {
|
||||
cancelStructuredTurn,
|
||||
resolveStructuredApproval,
|
||||
resolveStructuredElicitation,
|
||||
sendStructuredPrompt,
|
||||
setStructuredConfig,
|
||||
uploadStructuredFile,
|
||||
@@ -29,6 +30,7 @@ import { formatContextTokens, formatElapsed } from "./render.js";
|
||||
import type {
|
||||
AcpSessionConfigOption,
|
||||
StructuredApprovalDecision,
|
||||
StructuredElicitationAction,
|
||||
TranscriptRow as Row,
|
||||
Uuid,
|
||||
} from "../../types.js";
|
||||
@@ -36,6 +38,7 @@ import { Composer } from "./Composer.js";
|
||||
import {
|
||||
commandsFromTranscript,
|
||||
pendingApprovalRowId as pendingApprovalRowIdOf,
|
||||
pendingElicitationRowId as pendingElicitationRowIdOf,
|
||||
transcriptItems,
|
||||
turnIsActive,
|
||||
} from "./model.js";
|
||||
@@ -124,9 +127,10 @@ export const StructuredPane = forwardRef<StructuredPaneHandle, StructuredPanePro
|
||||
|
||||
const turnActive = turnIsActive(state);
|
||||
const pendingRowId = useMemo(() => pendingApprovalRowIdOf(rows, state), [rows, state]);
|
||||
const pendingElicitationRowId = useMemo(() => pendingElicitationRowIdOf(rows, state), [rows, state]);
|
||||
const items = useMemo(
|
||||
() => transcriptItems(rows, pendingRowId, turnActive),
|
||||
[rows, pendingRowId, turnActive],
|
||||
() => transcriptItems(rows, pendingRowId, turnActive, pendingElicitationRowId),
|
||||
[rows, pendingRowId, turnActive, pendingElicitationRowId],
|
||||
);
|
||||
const commands = useMemo(() => commandsFromTranscript(rows), [rows]);
|
||||
const configOptions: AcpSessionConfigOption[] = state.config_options?.configOptions ?? [];
|
||||
@@ -193,6 +197,20 @@ export const StructuredPane = forwardRef<StructuredPaneHandle, StructuredPanePro
|
||||
[sessionId, state.pending_approval?.nonce, pendingRowId],
|
||||
);
|
||||
|
||||
const decideElicitation = useCallback(
|
||||
async (rowId: string, action: StructuredElicitationAction): Promise<void> => {
|
||||
const nonce = state.pending_elicitation?.nonce;
|
||||
if (!nonce || rowId !== pendingElicitationRowId) return;
|
||||
try {
|
||||
await resolveStructuredElicitation(sessionId, nonce, action);
|
||||
} catch (e: unknown) {
|
||||
setError(String(e));
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
[sessionId, state.pending_elicitation?.nonce, pendingElicitationRowId],
|
||||
);
|
||||
|
||||
const dispatchPrompt = useCallback(
|
||||
(text: string, files: File[]) => {
|
||||
const generation = ++submission.current;
|
||||
@@ -311,6 +329,8 @@ export const StructuredPane = forwardRef<StructuredPaneHandle, StructuredPanePro
|
||||
row={row}
|
||||
pendingApproval={row.id === pendingRowId}
|
||||
onDecide={(decision) => decide(row.id, decision)}
|
||||
pendingElicitation={row.id === pendingElicitationRowId}
|
||||
onElicitationDecide={(action) => decideElicitation(row.id, action)}
|
||||
expanded={expandedRows.has(row.id)}
|
||||
onExpandedChange={(expanded) => setRowExpanded(row.id, expanded)}
|
||||
imageUrls={imageUrls.current}
|
||||
|
||||
@@ -10,14 +10,17 @@ import { useEffect, useRef, type ReactNode } from "react";
|
||||
import { renderInlineMarkdown, renderMarkdown, formatElapsed } from "./render.js";
|
||||
import type {
|
||||
AcpContentBlock,
|
||||
AcpElicitationSchema,
|
||||
AcpPermissionOption,
|
||||
AcpToolCallContent,
|
||||
AcpToolCallLike,
|
||||
StructuredApprovalDecision,
|
||||
StructuredElicitationAction,
|
||||
TranscriptRow as Row,
|
||||
TurnMeta,
|
||||
} from "../../types.js";
|
||||
import { ApprovalView } from "./ApprovalView.js";
|
||||
import { ElicitationView } from "./ElicitationView.js";
|
||||
import { Copyable } from "./Copyable.js";
|
||||
import { DiffView } from "./DiffView.js";
|
||||
import {
|
||||
@@ -337,6 +340,9 @@ export interface TranscriptRowProps {
|
||||
* rules). Every other approval row is history. */
|
||||
pendingApproval?: boolean;
|
||||
onDecide?: (decision: StructuredApprovalDecision) => Promise<void>;
|
||||
/** This row is the one live elicitation. */
|
||||
pendingElicitation?: boolean;
|
||||
onElicitationDecide?: (action: StructuredElicitationAction) => Promise<void>;
|
||||
expanded?: boolean;
|
||||
onExpandedChange?: (expanded: boolean) => void;
|
||||
imageUrls?: Map<string, string>;
|
||||
@@ -348,6 +354,8 @@ export function TranscriptRow({
|
||||
row,
|
||||
pendingApproval = false,
|
||||
onDecide,
|
||||
pendingElicitation = false,
|
||||
onElicitationDecide,
|
||||
expanded = false,
|
||||
onExpandedChange,
|
||||
imageUrls,
|
||||
@@ -374,6 +382,16 @@ export function TranscriptRow({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "elicitation": {
|
||||
const request = (row.data ?? {}) as { message?: string; requestedSchema?: AcpElicitationSchema };
|
||||
return (
|
||||
<ElicitationView
|
||||
request={request}
|
||||
pending={pendingElicitation}
|
||||
onDecide={onElicitationDecide ?? (() => Promise.resolve())}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "plan":
|
||||
return null;
|
||||
case "notice": {
|
||||
|
||||
@@ -134,6 +134,11 @@ export function pendingApprovalRowId(rows: TranscriptRow[], state: ReducedState)
|
||||
return state.pending_approval ? lastRowIdOfKind(rows, "approval") : null;
|
||||
}
|
||||
|
||||
/** Same for elicitations — one live at a time. */
|
||||
export function pendingElicitationRowId(rows: TranscriptRow[], state: ReducedState): string | null {
|
||||
return state.pending_elicitation ? lastRowIdOfKind(rows, "elicitation") : null;
|
||||
}
|
||||
|
||||
export function lastRowIdOfKind(rows: TranscriptRow[], kind: TranscriptRowKind): string | null {
|
||||
for (let i = rows.length - 1; i >= 0; i--) if (rows[i].kind === kind) return rows[i].id;
|
||||
return null;
|
||||
@@ -168,6 +173,7 @@ export function activityGroupsByTurn(
|
||||
rows: TranscriptRow[],
|
||||
pendingApprovalId: string | null,
|
||||
_turnActive: boolean,
|
||||
pendingElicitationId: string | null = null,
|
||||
): Map<string, ActivityGroup> {
|
||||
const groups = new Map<string, ActivityGroup>();
|
||||
let turnId = "before-first-message";
|
||||
@@ -204,8 +210,10 @@ export function activityGroupsByTurn(
|
||||
flushRun();
|
||||
continue;
|
||||
}
|
||||
const isResolvedApproval = row.kind === "approval" && row.id !== pendingApprovalId;
|
||||
if (isResolvedApproval) {
|
||||
const isResolvedInteraction =
|
||||
(row.kind === "approval" && row.id !== pendingApprovalId) ||
|
||||
(row.kind === "elicitation" && row.id !== pendingElicitationId);
|
||||
if (isResolvedInteraction) {
|
||||
runPending.push(row);
|
||||
continue;
|
||||
}
|
||||
@@ -239,8 +247,9 @@ export function transcriptItems(
|
||||
rows: TranscriptRow[],
|
||||
pendingApprovalId: string | null,
|
||||
turnActive: boolean,
|
||||
pendingElicitationId: string | null = null,
|
||||
): TranscriptItem[] {
|
||||
const groups = activityGroupsByTurn(rows, pendingApprovalId, turnActive);
|
||||
const groups = activityGroupsByTurn(rows, pendingApprovalId, turnActive, pendingElicitationId);
|
||||
// Reverse index: row id → group id that contains it.
|
||||
const rowToGroup = new Map<string, string>();
|
||||
for (const [groupId, group] of groups) for (const r of group.rows) rowToGroup.set(r.id, groupId);
|
||||
@@ -259,9 +268,10 @@ export function transcriptItems(
|
||||
}
|
||||
if (row.kind === "plan") continue;
|
||||
if (row.kind === "notice" && isStateOnlyNotice(row)) continue;
|
||||
// A resolved approval outside a group adds nothing the tool row does not
|
||||
// already say. A pending one always renders.
|
||||
// A resolved approval/elicitation outside a group adds nothing the tool
|
||||
// row does not already say. A pending one always renders.
|
||||
if (row.kind === "approval" && row.id !== pendingApprovalId) continue;
|
||||
if (row.kind === "elicitation" && row.id !== pendingElicitationId) continue;
|
||||
items.push({ type: "row", row });
|
||||
}
|
||||
|
||||
|
||||
@@ -913,6 +913,90 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Elicitation: the structured question the agent asks the operator.
|
||||
Visually parallel to approvals but accent-tinted instead of warn-tinted. */
|
||||
.st-elicitation {
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--r-2);
|
||||
background: color-mix(in oklab, var(--accent) 10%, var(--surface));
|
||||
box-shadow: var(--shadow-1);
|
||||
padding: var(--sp-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.st-elicitation-title {
|
||||
color: var(--accent);
|
||||
font-size: var(--fs-11);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.st-elicitation-options {
|
||||
display: flex;
|
||||
gap: var(--sp-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.st-elicitation-acts {
|
||||
display: flex;
|
||||
gap: var(--sp-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.st-elicitation-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.st-elicitation-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-1);
|
||||
font-size: var(--fs-12);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.st-elicitation-field input[type="text"],
|
||||
.st-elicitation-field input[type="number"] {
|
||||
height: 30px;
|
||||
padding: 0 var(--sp-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-1);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.st-elicitation-field input[type="checkbox"] {
|
||||
margin-right: var(--sp-1);
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.st-elicitation-required {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.st-elicitation-desc {
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-11);
|
||||
}
|
||||
|
||||
.st-elicitation-compact {
|
||||
font-size: var(--fs-11);
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
.st-row-elicitation:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.act {
|
||||
appearance: none;
|
||||
font: inherit;
|
||||
|
||||
@@ -67,6 +67,12 @@ export function structuredApproval(id: Uuid, nonce: string): string {
|
||||
return `${session(id)}/structured/approvals/${encodeURIComponent(nonce)}`;
|
||||
}
|
||||
|
||||
/** `POST /api/sessions/:id/structured/elicitations/:nonce` — answer an
|
||||
* elicitation request. */
|
||||
export function structuredElicitation(id: Uuid, nonce: string): string {
|
||||
return `${session(id)}/structured/elicitations/${encodeURIComponent(nonce)}`;
|
||||
}
|
||||
|
||||
/** Append the `path` query parameter `DIRS` and `GITROOT` accept. */
|
||||
export function withPathQuery(route: string, value: string): string {
|
||||
return `${route}?${PATH_QUERY}=${encodeURIComponent(value)}`;
|
||||
|
||||
@@ -395,6 +395,7 @@ export type TranscriptRowKind =
|
||||
| "tool_call"
|
||||
| "tool_diff"
|
||||
| "approval"
|
||||
| "elicitation"
|
||||
| "plan"
|
||||
| "notice";
|
||||
|
||||
@@ -435,6 +436,7 @@ export interface TurnState {
|
||||
export interface ReducedState {
|
||||
turn: TurnState;
|
||||
pending_approval?: { nonce: string; request: AcpRequestPermissionRequest } | null;
|
||||
pending_elicitation?: { nonce: string; message?: string; schema?: AcpElicitationSchema } | null;
|
||||
plan?: AcpPlan | null;
|
||||
usage?: AcpUsage | null;
|
||||
context?: AcpUsage | null;
|
||||
@@ -486,6 +488,35 @@ export interface AcpSessionConfigOption {
|
||||
* string (a bracketless Rust enum), not `{decision: ...}`. */
|
||||
export type StructuredApprovalDecision = "allow_once" | "allow_for_session" | "deny";
|
||||
|
||||
/** `POST /api/sessions/:id/structured/elicitations/:nonce` body. */
|
||||
export type StructuredElicitationAction =
|
||||
| { action: "accept"; content: Record<string, string | number | boolean> }
|
||||
| { action: "decline" }
|
||||
| { action: "cancel" };
|
||||
|
||||
/** Property schema within an ACP elicitation request. */
|
||||
export interface AcpElicitationProperty {
|
||||
type?: "string" | "number" | "integer" | "boolean";
|
||||
title?: string;
|
||||
description?: string;
|
||||
default?: string | number | boolean;
|
||||
enum?: string[];
|
||||
oneOf?: Array<{ const: string; title: string; description?: string }>;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
}
|
||||
|
||||
/** The `requested_schema` carried by an ACP `elicitation/create` request. */
|
||||
export interface AcpElicitationSchema {
|
||||
type?: "object";
|
||||
title?: string;
|
||||
description?: string;
|
||||
properties: Record<string, AcpElicitationProperty>;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
// ---- ACP v1 payload shapes (agent-client-protocol schema, camelCase) ----
|
||||
|
||||
export interface AcpEmbeddedResource {
|
||||
|
||||
Reference in New Issue
Block a user