Merge pull request #15 from phj1081/codex/parse-fenced-ejclaw-output
[codex] Parse fenced EJClaw output envelopes
This commit is contained in:
@@ -41,6 +41,7 @@ For locally generated images or e2e screenshots that should appear in Discord, p
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- When emitting this as your final runner output, emit the JSON envelope directly. Do not wrap it in Markdown fences or add prose outside the JSON.
|
||||||
- Use absolute local paths only
|
- Use absolute local paths only
|
||||||
- Do not duplicate the same path in the visible text
|
- Do not duplicate the same path in the visible text
|
||||||
- Supported attachment formats are raster image files: PNG, JPEG, GIF, WebP, and BMP. SVG is not accepted.
|
- Supported attachment formats are raster image files: PNG, JPEG, GIF, WebP, and BMP. SVG is not accepted.
|
||||||
|
|||||||
@@ -124,6 +124,13 @@ function normalizeAttachments(value: unknown): RunnerOutputAttachment[] {
|
|||||||
return attachments;
|
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(
|
export function normalizeEjclawStructuredOutput(
|
||||||
result: string | null,
|
result: string | null,
|
||||||
): NormalizedRunnerOutput {
|
): NormalizedRunnerOutput {
|
||||||
@@ -132,8 +139,9 @@ export function normalizeEjclawStructuredOutput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trimmed = result.trim();
|
const trimmed = result.trim();
|
||||||
|
const jsonCandidate = extractStructuredJsonCandidate(trimmed);
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed) as {
|
const parsed = JSON.parse(jsonCandidate) as {
|
||||||
ejclaw?: {
|
ejclaw?: {
|
||||||
visibility?: unknown;
|
visibility?: unknown;
|
||||||
text?: 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', () => {
|
it('falls back to visible raw text on invalid public verdicts', () => {
|
||||||
const raw = JSON.stringify({
|
const raw = JSON.stringify({
|
||||||
ejclaw: {
|
ejclaw: {
|
||||||
|
|||||||
Reference in New Issue
Block a user