UNPKG

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
// 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