feat: add workflow-wave-plan skill and A2UI debug logging

- Add CSV Wave planning and execution skill (explore via wave → conflict
  resolution → execution CSV with linked exploration context)
- Add debug NDJSON logging for A2UI submit-all and multi-answer polling
This commit is contained in:
catlog22
2026-02-28 11:40:28 +08:00
parent 0a37bc3eaf
commit 67e78b132c
3 changed files with 920 additions and 54 deletions

View File

@@ -341,6 +341,77 @@ export class A2UIWebSocketHandler {
): boolean {
const params = action.parameters ?? {};
const questionId = typeof params.questionId === 'string' ? params.questionId : undefined;
// Handle submit-all first - it uses compositeId instead of questionId
if (action.actionId === 'submit-all') {
const compositeId = typeof params.compositeId === 'string' ? params.compositeId : undefined;
const questionIds = Array.isArray(params.questionIds) ? params.questionIds as string[] : undefined;
if (!compositeId || !questionIds) {
return false;
}
// DEBUG: NDJSON log for submit-all received
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H2',
event: 'submit_all_received_in_handleQuestionAction',
compositeId,
questionCount: questionIds.length
}));
// Collect answers for all sub-questions
const answers: QuestionAnswer[] = [];
for (const qId of questionIds) {
const singleSel = this.singleSelectSelections.get(qId);
const multiSel = this.multiSelectSelections.get(qId);
const inputVal = this.inputValues.get(qId);
const otherText = this.inputValues.get(`__other__:${qId}`);
if (singleSel !== undefined) {
const value = singleSel === '__other__' && otherText ? otherText : singleSel;
answers.push({ questionId: qId, value, cancelled: false });
} else if (multiSel !== undefined) {
const values = Array.from(multiSel).map(v =>
v === '__other__' && otherText ? otherText : v
);
answers.push({ questionId: qId, value: values, cancelled: false });
} else if (inputVal !== undefined) {
answers.push({ questionId: qId, value: inputVal, cancelled: false });
} else {
answers.push({ questionId: qId, value: '', cancelled: false });
}
// Cleanup per-question tracking
this.singleSelectSelections.delete(qId);
this.multiSelectSelections.delete(qId);
this.inputValues.delete(qId);
this.inputValues.delete(`__other__:${qId}`);
}
// Call multi-answer callback
let handled = false;
if (this.multiAnswerCallback) {
handled = this.multiAnswerCallback(compositeId, answers);
}
if (!handled) {
// Store for HTTP polling retrieval
this.resolvedMultiAnswers.set(compositeId, { compositeId, answers, timestamp: Date.now() });
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H2',
event: 'answer_stored_for_polling',
compositeId,
answerCount: answers.length
}));
}
this.activeSurfaces.delete(compositeId);
return true;
}
// For other actions, questionId is required
if (!questionId) {
return false;
}
@@ -446,60 +517,6 @@ export class A2UIWebSocketHandler {
return true;
}
case 'submit-all': {
// Multi-question composite submit
const compositeId = typeof params.compositeId === 'string' ? params.compositeId : undefined;
const questionIds = Array.isArray(params.questionIds) ? params.questionIds as string[] : undefined;
if (!compositeId || !questionIds) {
return false;
}
// Collect answers for all sub-questions
const answers: QuestionAnswer[] = [];
for (const qId of questionIds) {
const singleSel = this.singleSelectSelections.get(qId);
const multiSel = this.multiSelectSelections.get(qId);
const inputVal = this.inputValues.get(qId);
const otherText = this.inputValues.get(`__other__:${qId}`);
if (singleSel !== undefined) {
// Resolve __other__ to actual text input
const value = singleSel === '__other__' && otherText ? otherText : singleSel;
answers.push({ questionId: qId, value, cancelled: false });
} else if (multiSel !== undefined) {
// Resolve __other__ in multi-select: replace with actual text
const values = Array.from(multiSel).map(v =>
v === '__other__' && otherText ? otherText : v
);
answers.push({ questionId: qId, value: values, cancelled: false });
} else if (inputVal !== undefined) {
answers.push({ questionId: qId, value: inputVal, cancelled: false });
} else {
// No value recorded — include empty
answers.push({ questionId: qId, value: '', cancelled: false });
}
// Cleanup per-question tracking
this.singleSelectSelections.delete(qId);
this.multiSelectSelections.delete(qId);
this.inputValues.delete(qId);
this.inputValues.delete(`__other__:${qId}`);
}
// Call multi-answer callback
let handled = false;
if (this.multiAnswerCallback) {
handled = this.multiAnswerCallback(compositeId, answers);
}
if (!handled) {
// Store for HTTP polling retrieval
this.resolvedMultiAnswers.set(compositeId, { compositeId, answers, timestamp: Date.now() });
}
// Always clean up UI state
this.activeSurfaces.delete(compositeId);
return true;
}
default:
return false;
}

View File

@@ -611,8 +611,29 @@ export function handleAnswer(answer: QuestionAnswer): boolean {
* @returns True if answer was processed
*/
export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]): boolean {
const handleStartTime = Date.now();
// DEBUG: NDJSON log for handleMultiAnswer start
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H3',
event: 'handle_multi_answer_start',
compositeId,
answerCount: answers.length
}));
const pending = getPendingQuestion(compositeId);
if (!pending) {
// DEBUG: NDJSON log for missing pending question
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H3',
event: 'handle_multi_answer_no_pending',
compositeId,
elapsedMs: Date.now() - handleStartTime
}));
return false;
}
@@ -625,6 +646,17 @@ export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]
});
removePendingQuestion(compositeId);
// DEBUG: NDJSON log for handleMultiAnswer complete
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H3',
event: 'handle_multi_answer_complete',
compositeId,
elapsedMs: Date.now() - handleStartTime
}));
return true;
}
@@ -637,9 +669,24 @@ export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]
*/
function startAnswerPolling(questionId: string, isComposite: boolean = false): void {
const pollPath = `/api/a2ui/answer?questionId=${encodeURIComponent(questionId)}&composite=${isComposite}`;
const startTime = Date.now();
// DEBUG: NDJSON log for polling start
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H1',
event: 'polling_start',
questionId,
isComposite,
port: DASHBOARD_PORT,
firstPollDelayMs: POLL_INTERVAL_MS
}));
console.error(`[A2UI-Poll] Starting polling for questionId=${questionId}, composite=${isComposite}, port=${DASHBOARD_PORT}`);
let pollCount = 0;
const poll = () => {
// Stop if the question was already resolved or timed out
if (!hasPendingQuestion(questionId)) {
@@ -647,6 +694,20 @@ function startAnswerPolling(questionId: string, isComposite: boolean = false): v
return;
}
pollCount++;
const pollStartTime = Date.now();
// DEBUG: NDJSON log for each poll attempt
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H1',
event: 'poll_attempt',
questionId,
pollCount,
elapsedMs: pollStartTime - startTime
}));
const req = http.get({ hostname: '127.0.0.1', port: DASHBOARD_PORT, path: pollPath, timeout: 2000 }, (res) => {
let data = '';
res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
@@ -667,6 +728,20 @@ function startAnswerPolling(questionId: string, isComposite: boolean = false): v
console.error(`[A2UI-Poll] Answer received for questionId=${questionId}:`, JSON.stringify(parsed).slice(0, 200));
// DEBUG: NDJSON log for answer received
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'DEBUG',
hid: 'H1',
event: 'answer_received',
questionId,
pollCount,
elapsedMs: Date.now() - startTime,
pollLatencyMs: Date.now() - pollStartTime,
isComposite,
answerPreview: JSON.stringify(parsed).slice(0, 100)
}));
if (isComposite && Array.isArray(parsed.answers)) {
const ok = handleMultiAnswer(questionId, parsed.answers as QuestionAnswer[]);
console.error(`[A2UI-Poll] handleMultiAnswer result: ${ok}`);