aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
181 lines • 6.8 kB
JavaScript
// HITL prompt envelope extractor + delivery adapter interface.
//
// Per `agentic-sandbox/docs/contracts/extensions/hitl-prompt/v1/spec.md`,
// when a task transitions to `input-required` the executor places a HITL
// prompt envelope at:
//
// task.status.message.metadata['https://agentic-sandbox.aiwg.io/extensions/hitl-prompt/v1']
//
// The envelope has required keys (prompt_id, prompt, response_schema) and
// optional keys (deadline, allowed_responders). AIWG's orchestrator owns
// the workflow:
//
// 1. Detect `input-required` state on a Task / status-update event
// 2. Extract the envelope
// 3. Route to a HitlDeliveryAdapter (CLI / Slack / web)
// 4. Validate the operator's response against `response_schema`
// 5. POST a reply Message with `metadata.hitl_response_for: <prompt_id>`
// and the response payload at `metadata.<URI>`
//
// This module handles steps 1–4. Step 5 lives in the A2AClient consumer
// that drives the task lifecycle.
//
// @issue #1255
import { A2A_HITL_PROMPT_V1 } from './client.js';
/** Required envelope keys per spec §Prompt envelope. */
const REQUIRED_ENVELOPE_KEYS = ['prompt_id', 'prompt', 'response_schema'];
/**
* Pull the HITL envelope out of a Task / TaskStatus / Message metadata
* blob. Returns `null` when the structure is not `input-required` or
* doesn't carry the envelope. Returns a typed envelope when valid.
*
* Pass either the full Task, just the TaskStatus, or just the
* `status.message` to suit the calling site.
*/
export function extractHitlEnvelope(source) {
if (!source)
return null;
// Get to the `metadata` object that should carry the envelope.
let metadata;
let stateGuardPassed = false;
if (isTask(source)) {
if (source.status.state !== 'input-required')
return null;
stateGuardPassed = true;
metadata = source.status.message?.metadata;
}
else if (isTaskStatus(source)) {
if (source.state !== 'input-required')
return null;
stateGuardPassed = true;
metadata = source.message?.metadata;
}
else if (isMessage(source)) {
// No state to guard on; assume caller already filtered by state.
stateGuardPassed = true;
metadata = source.metadata;
}
if (!stateGuardPassed)
return null;
if (!metadata) {
return { ok: false, reason: 'no metadata on status.message' };
}
const raw = metadata[A2A_HITL_PROMPT_V1];
if (raw === undefined) {
return { ok: false, reason: `missing envelope at metadata[${A2A_HITL_PROMPT_V1}]` };
}
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
return { ok: false, reason: 'envelope is not an object' };
}
const env = raw;
for (const key of REQUIRED_ENVELOPE_KEYS) {
if (!(key in env)) {
return { ok: false, reason: `envelope missing required key: ${key}` };
}
}
if (typeof env['prompt_id'] !== 'string' || env['prompt_id'].length === 0) {
return { ok: false, reason: 'prompt_id must be a non-empty string' };
}
if (typeof env['prompt'] !== 'string') {
return { ok: false, reason: 'prompt must be a string' };
}
return { ok: true, envelope: env };
}
/** Build the response Message that closes a HITL prompt cycle. */
export function buildHitlResponseMessage(opts) {
const message = {
messageId: opts.messageId,
role: 'user',
parts: [
{
kind: 'data',
data: opts.response,
},
],
metadata: {
hitl_response_for: opts.promptId,
[A2A_HITL_PROMPT_V1]: { response: opts.response },
},
};
if (opts.taskId !== undefined)
message.taskId = opts.taskId;
if (opts.contextId !== undefined)
message.contextId = opts.contextId;
return message;
}
/**
* Validate a response payload against the envelope's `response_schema`.
*
* This module deliberately stays free of an `ajv` import so consumers can
* either plug in their own validator (see `validateResponseAgainstSchema`)
* or fall back to structural checks. AIWG already has `ajv` in the dep
* graph from `src/research/`, so the typical wiring is:
*
* import Ajv from 'ajv';
* const ajv = new Ajv({ allErrors: true });
* const validate = ajv.compile(envelope.response_schema);
* const ok = validate(response);
* if (!ok) { ... use validate.errors ... }
*
* For tests we ship a tiny structural validator below that covers the
* happy path. A full `ajv` wiring lands at the orchestrator call site so
* AIWG can pick its own validator strategy (Ajv strict mode, Zod, etc.).
*/
export function validateResponseStructurally(schema, response) {
const errors = [];
walk(schema, response, '$', errors);
return errors.length === 0 ? { ok: true } : { ok: false, errors };
}
function walk(schema, value, path, errors) {
if (schema === null || typeof schema !== 'object' || Array.isArray(schema))
return;
const s = schema;
if (typeof s['type'] === 'string') {
const expected = s['type'];
const actual = typeOf(value);
if (expected !== actual) {
errors.push(`${path}: expected ${expected}, got ${actual}`);
return;
}
}
if (s['type'] === 'object' && value !== null && typeof value === 'object' && !Array.isArray(value)) {
const v = value;
const required = Array.isArray(s['required']) ? s['required'] : [];
for (const r of required) {
if (typeof r === 'string' && !(r in v)) {
errors.push(`${path}.${r}: required field missing`);
}
}
const properties = s['properties'] && typeof s['properties'] === 'object' && !Array.isArray(s['properties'])
? s['properties']
: {};
for (const [k, sub] of Object.entries(properties)) {
if (k in v)
walk(sub, v[k], `${path}.${k}`, errors);
}
}
if (s['type'] === 'array' && Array.isArray(value)) {
const items = s['items'];
if (items) {
value.forEach((el, i) => walk(items, el, `${path}[${i}]`, errors));
}
}
}
function typeOf(v) {
if (v === null)
return 'null';
if (Array.isArray(v))
return 'array';
return typeof v;
}
// ---------- narrow type guards ----------
function isTask(v) {
return typeof v === 'object' && v !== null && 'id' in v && 'status' in v;
}
function isTaskStatus(v) {
return typeof v === 'object' && v !== null && 'state' in v && !('id' in v);
}
function isMessage(v) {
return typeof v === 'object' && v !== null && 'messageId' in v && 'role' in v;
}
//# sourceMappingURL=hitl.js.map