Parse fenced EJClaw output envelopes
This commit is contained in:
@@ -124,6 +124,13 @@ function normalizeAttachments(value: unknown): RunnerOutputAttachment[] {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
function extractStructuredJsonCandidate(trimmed: string): string {
|
||||
const fencedJson = trimmed.match(
|
||||
/^```(?:json|JSON)?[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/,
|
||||
);
|
||||
return fencedJson?.[1]?.trim() ?? trimmed;
|
||||
}
|
||||
|
||||
export function normalizeEjclawStructuredOutput(
|
||||
result: string | null,
|
||||
): NormalizedRunnerOutput {
|
||||
@@ -132,8 +139,9 @@ export function normalizeEjclawStructuredOutput(
|
||||
}
|
||||
|
||||
const trimmed = result.trim();
|
||||
const jsonCandidate = extractStructuredJsonCandidate(trimmed);
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as {
|
||||
const parsed = JSON.parse(jsonCandidate) as {
|
||||
ejclaw?: {
|
||||
visibility?: unknown;
|
||||
text?: unknown;
|
||||
|
||||
@@ -86,6 +86,104 @@ describe('shared agent protocol helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('parses fenced public ejclaw attachments', () => {
|
||||
expect(
|
||||
normalizeEjclawStructuredOutput(`\`\`\`json
|
||||
{
|
||||
"ejclaw": {
|
||||
"visibility": "public",
|
||||
"text": "검 아이콘을 생성했습니다.",
|
||||
"verdict": "done",
|
||||
"attachments": [
|
||||
{
|
||||
"path": "/tmp/imagegen-sword.png",
|
||||
"name": "imagegen-sword.png",
|
||||
"mime": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
\`\`\``),
|
||||
).toEqual({
|
||||
result: '검 아이콘을 생성했습니다.',
|
||||
output: {
|
||||
visibility: 'public',
|
||||
text: '검 아이콘을 생성했습니다.',
|
||||
verdict: 'done',
|
||||
attachments: [
|
||||
{
|
||||
path: '/tmp/imagegen-sword.png',
|
||||
name: 'imagegen-sword.png',
|
||||
mime: 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses unlabeled fenced ejclaw envelopes', () => {
|
||||
expect(
|
||||
normalizeEjclawStructuredOutput(`\`\`\`
|
||||
{"ejclaw":{"visibility":"public","text":"스크린샷입니다.","verdict":"done"}}
|
||||
\`\`\``),
|
||||
).toEqual({
|
||||
result: '스크린샷입니다.',
|
||||
output: {
|
||||
visibility: 'public',
|
||||
text: '스크린샷입니다.',
|
||||
verdict: 'done',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not parse mixed prose and fenced ejclaw JSON as structured output', () => {
|
||||
const raw = `설명입니다.
|
||||
|
||||
\`\`\`json
|
||||
{"ejclaw":{"visibility":"public","text":"첨부입니다.","verdict":"done"}}
|
||||
\`\`\``;
|
||||
|
||||
expect(normalizeEjclawStructuredOutput(raw)).toEqual({
|
||||
result: raw,
|
||||
output: {
|
||||
visibility: 'public',
|
||||
text: raw,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back when fenced JSON has no ejclaw envelope', () => {
|
||||
const raw = `\`\`\`json
|
||||
{"text":"plain json"}
|
||||
\`\`\``;
|
||||
|
||||
expect(normalizeEjclawStructuredOutput(raw)).toEqual({
|
||||
result: raw,
|
||||
output: {
|
||||
visibility: 'public',
|
||||
text: raw,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back when multiple fenced JSON blocks are present', () => {
|
||||
const raw = `\`\`\`json
|
||||
{"ejclaw":{"visibility":"public","text":"첫 번째","verdict":"done"}}
|
||||
\`\`\`
|
||||
|
||||
\`\`\`json
|
||||
{"ejclaw":{"visibility":"public","text":"두 번째","verdict":"done"}}
|
||||
\`\`\``;
|
||||
|
||||
expect(normalizeEjclawStructuredOutput(raw)).toEqual({
|
||||
result: raw,
|
||||
output: {
|
||||
visibility: 'public',
|
||||
text: raw,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to visible raw text on invalid public verdicts', () => {
|
||||
const raw = JSON.stringify({
|
||||
ejclaw: {
|
||||
|
||||
Reference in New Issue
Block a user