573e9ec909
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The `grok-local` adapter streams reasoning text to the issue "Working..." panel as the grok CLI runs > - The `grok` CLI's `--output-format streaming-json` mode silently drops the `\n` separator between reasoning turns around tool calls > - Consecutive `thought` chunks (e.g. `` "`" `` followed by `"The"`) arrive with no intervening whitespace event, so the UI's `delta: true` concatenator merged them into run-on text like `"…planningGreat, now I have the issue descriptionThe only co"` > - This PR adds a small turn-boundary helper that detects sentence boundaries in the upstream `thought` stream and inserts a single `\n` only when the previous chunk ended with sentence punctuation (or a balanced closing backtick) AND the next chunk begins a new uppercase sentence > - The benefit is readable streaming reasoning in the UI without changing how completed messages are stored ## What Changed - Added `packages/adapters/grok-local/src/shared/turn-boundary.ts` with per-stream state (last chunk + backtick parity) and a `restoreTurnBoundary()` helper that inserts `\n` only between balanced, sentence-terminated `thought` chunks - Wired the helper into `parseGrokJsonl` (server) and added a new `createGrokStdoutParser` factory used by `grokLocalUIAdapter` for the live "Working..." panel - Added focused tests in `shared/turn-boundary.test.ts`, plus regression assertions in `server/parse.test.ts` and `ui/parse-stdout.test.ts` ## Verification - `pnpm --filter @paperclip/grok-local test` — 23/23 adapter tests pass - `pnpm --filter @paperclip/grok-local typecheck` and UI typecheck — clean - Replayed an actual broken `grok 0.1.210` stream from the report; previously-merged boundaries (`` `ls`The ``, `returned:Confirmed`) now render with a separating newline; chunks inside un-closed backtick spans are left alone ## Risks - Low risk. Boundary insertion only fires when prev ends with `.`/`!`/`?`/balanced `` ` `` and next begins with an uppercase ≥2-char word, with no whitespace on either side. Worst case: a rare missed split or a misplaced newline inside reasoning — both purely cosmetic and confined to the live streaming panel. ## Model Used - Claude Opus 4.7 (claude-opus-4-7), Anthropic, extended thinking + tool use via Claude Code ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
import { asString, parseJson, parseObject } from "@paperclipai/adapter-utils/server-utils";
|
|
import { applyTurnBoundary, createTurnBoundaryState } from "../shared/turn-boundary.js";
|
|
|
|
export interface ParsedGrokJsonl {
|
|
sessionId: string | null;
|
|
summary: string;
|
|
thought: string;
|
|
errorMessage: string | null;
|
|
stopReason: string | null;
|
|
requestId: string | null;
|
|
}
|
|
|
|
function errorText(value: unknown): string {
|
|
if (typeof value === "string") return value;
|
|
const rec = parseObject(value);
|
|
const message =
|
|
asString(rec.message, "").trim() ||
|
|
asString(rec.error, "").trim() ||
|
|
asString(rec.detail, "").trim() ||
|
|
asString(rec.code, "").trim();
|
|
if (message) return message;
|
|
try {
|
|
return JSON.stringify(rec);
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
export function parseGrokJsonl(stdout: string): ParsedGrokJsonl {
|
|
let sessionId: string | null = null;
|
|
let stopReason: string | null = null;
|
|
let requestId: string | null = null;
|
|
let errorMessage: string | null = null;
|
|
const thoughtParts: string[] = [];
|
|
const textParts: string[] = [];
|
|
const thoughtBoundary = createTurnBoundaryState();
|
|
|
|
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
const line = rawLine.trim();
|
|
if (!line) continue;
|
|
|
|
const event = parseJson(line);
|
|
if (!event) continue;
|
|
|
|
const type = asString(event.type, "").trim();
|
|
if (type === "thought") {
|
|
const text = asString(event.data, "");
|
|
if (text) thoughtParts.push(applyTurnBoundary(thoughtBoundary, text));
|
|
continue;
|
|
}
|
|
|
|
if (type === "text") {
|
|
const text = asString(event.data, "");
|
|
if (text) textParts.push(text);
|
|
continue;
|
|
}
|
|
|
|
if (type === "end") {
|
|
sessionId = asString(event.sessionId, "").trim() || sessionId;
|
|
stopReason = asString(event.stopReason, "").trim() || stopReason;
|
|
requestId = asString(event.requestId, "").trim() || requestId;
|
|
continue;
|
|
}
|
|
|
|
if (type === "error") {
|
|
const text = errorText(event.error ?? event.message ?? event.detail ?? event.data).trim();
|
|
if (text) errorMessage = text;
|
|
}
|
|
}
|
|
|
|
return {
|
|
sessionId,
|
|
summary: textParts.join("").trim(),
|
|
thought: thoughtParts.join("").trim(),
|
|
errorMessage,
|
|
stopReason,
|
|
requestId,
|
|
};
|
|
}
|
|
|
|
export function isGrokUnknownSessionError(stdout: string, stderr: string): boolean {
|
|
const haystack = `${stdout}\n${stderr}`
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
return /unknown\s+session|session(?:\s+.*)?\s+not\s+found|resume\s+.*\s+not\s+found|invalid\s+session/i.test(haystack);
|
|
}
|