feat(roadblock): collect and strip CLAUDEDO_BLOCKED markers in StreamAnalyzer
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Runner;
|
namespace ClaudeDo.Worker.Runner;
|
||||||
@@ -11,6 +12,7 @@ public sealed class StreamResult
|
|||||||
public int TokensIn { get; set; }
|
public int TokensIn { get; set; }
|
||||||
public int TokensOut { get; set; }
|
public int TokensOut { get; set; }
|
||||||
public int ApiRetryCount { get; set; }
|
public int ApiRetryCount { get; set; }
|
||||||
|
public IReadOnlyList<string> Blocks { get; set; } = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class StreamAnalyzer
|
public sealed class StreamAnalyzer
|
||||||
@@ -22,6 +24,8 @@ public sealed class StreamAnalyzer
|
|||||||
private int _tokensIn;
|
private int _tokensIn;
|
||||||
private int _tokensOut;
|
private int _tokensOut;
|
||||||
private int _apiRetryCount;
|
private int _apiRetryCount;
|
||||||
|
private readonly List<string> _blocks = new();
|
||||||
|
private const string BlockedPrefix = "CLAUDEDO_BLOCKED:";
|
||||||
|
|
||||||
public void ProcessLine(string ndjsonLine)
|
public void ProcessLine(string ndjsonLine)
|
||||||
{
|
{
|
||||||
@@ -39,7 +43,7 @@ public sealed class StreamAnalyzer
|
|||||||
{
|
{
|
||||||
case "result":
|
case "result":
|
||||||
if (root.TryGetProperty("result", out var resultProp))
|
if (root.TryGetProperty("result", out var resultProp))
|
||||||
_resultMarkdown = resultProp.GetString();
|
_resultMarkdown = StripAndCollect(resultProp.GetString());
|
||||||
if (root.TryGetProperty("structured_output", out var structuredProp))
|
if (root.TryGetProperty("structured_output", out var structuredProp))
|
||||||
_structuredOutputJson = structuredProp.ToString();
|
_structuredOutputJson = structuredProp.ToString();
|
||||||
if (root.TryGetProperty("session_id", out var sessionProp))
|
if (root.TryGetProperty("session_id", out var sessionProp))
|
||||||
@@ -56,6 +60,7 @@ public sealed class StreamAnalyzer
|
|||||||
|
|
||||||
case "assistant":
|
case "assistant":
|
||||||
_turnCount++;
|
_turnCount++;
|
||||||
|
CollectFromAssistant(root);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "system":
|
case "system":
|
||||||
@@ -81,6 +86,7 @@ public sealed class StreamAnalyzer
|
|||||||
TokensIn = _tokensIn,
|
TokensIn = _tokensIn,
|
||||||
TokensOut = _tokensOut,
|
TokensOut = _tokensOut,
|
||||||
ApiRetryCount = _apiRetryCount,
|
ApiRetryCount = _apiRetryCount,
|
||||||
|
Blocks = _blocks,
|
||||||
};
|
};
|
||||||
|
|
||||||
private string? FallbackResult()
|
private string? FallbackResult()
|
||||||
@@ -97,6 +103,37 @@ public sealed class StreamAnalyzer
|
|||||||
return _structuredOutputJson;
|
return _structuredOutputJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CollectFromAssistant(JsonElement root)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty("message", out var msg)) return;
|
||||||
|
if (msg.ValueKind != JsonValueKind.Object) return;
|
||||||
|
if (!msg.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array) return;
|
||||||
|
foreach (var block in content.EnumerateArray())
|
||||||
|
if (block.TryGetProperty("type", out var t) && t.GetString() == "text"
|
||||||
|
&& block.TryGetProperty("text", out var txt))
|
||||||
|
ScanForBlocks(txt.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScanForBlocks(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return;
|
||||||
|
foreach (var line in text.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (trimmed.StartsWith(BlockedPrefix, StringComparison.Ordinal))
|
||||||
|
_blocks.Add(trimmed[BlockedPrefix.Length..].Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? StripAndCollect(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return text;
|
||||||
|
ScanForBlocks(text);
|
||||||
|
var kept = text.Split('\n')
|
||||||
|
.Where(l => !l.Trim().StartsWith(BlockedPrefix, StringComparison.Ordinal));
|
||||||
|
return string.Join('\n', kept).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private void TryAccumulateUsage(JsonElement root)
|
private void TryAccumulateUsage(JsonElement root)
|
||||||
{
|
{
|
||||||
if (!root.TryGetProperty("event", out var eventProp)) return;
|
if (!root.TryGetProperty("event", out var eventProp)) return;
|
||||||
|
|||||||
@@ -120,4 +120,36 @@ public sealed class StreamAnalyzerTests
|
|||||||
Assert.Contains("output", result.ResultMarkdown);
|
Assert.Contains("output", result.ResultMarkdown);
|
||||||
Assert.Contains("42", result.ResultMarkdown);
|
Assert.Contains("42", result.ResultMarkdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Collects_Blocked_Markers_From_Assistant_Text()
|
||||||
|
{
|
||||||
|
var analyzer = new StreamAnalyzer();
|
||||||
|
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"working\nCLAUDEDO_BLOCKED: missing API key\nmoving on"}]}}""");
|
||||||
|
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"CLAUDEDO_BLOCKED: cannot reach db"}]}}""");
|
||||||
|
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
|
||||||
|
var result = analyzer.GetResult();
|
||||||
|
Assert.Equal(2, result.Blocks.Count);
|
||||||
|
Assert.Equal("missing API key", result.Blocks[0]);
|
||||||
|
Assert.Equal("cannot reach db", result.Blocks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Strips_Blocked_Markers_From_Result_Text()
|
||||||
|
{
|
||||||
|
var analyzer = new StreamAnalyzer();
|
||||||
|
analyzer.ProcessLine("""{"type":"result","result":"All set.\nCLAUDEDO_BLOCKED: no creds\nDone.","session_id":"s1"}""");
|
||||||
|
var result = analyzer.GetResult();
|
||||||
|
Assert.DoesNotContain("CLAUDEDO_BLOCKED", result.ResultMarkdown);
|
||||||
|
Assert.Single(result.Blocks);
|
||||||
|
Assert.Equal("no creds", result.Blocks[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void No_Markers_Means_Empty_Blocks()
|
||||||
|
{
|
||||||
|
var analyzer = new StreamAnalyzer();
|
||||||
|
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
|
||||||
|
Assert.Empty(analyzer.GetResult().Blocks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user