Read workflow-status telemetry from CLI transcript files

Dynamic ultracode workflows run as a detached background task with their
own transcript dir, so their subagent events never flow through the main
-p stream. The previous parent_tool_use_id tap therefore captured nothing
for real workflows and the status stayed "none" the whole run.

Replace the tap with a file-based reader:
- TapWorkflowLaunch parses the "Transcript dir:" line from the Workflow
  tool_result and registers the run.
- WorkflowProgressTracker becomes a per-session registry of transcript
  paths (no longer a stream accumulator).
- WorkflowTelemetryReader reads journal.jsonl for live counts (running)
  and <runId>.json for rich per-agent telemetry (completed); FinalizeChain
  persists the rich JSON into chains.workflow_info.
- Modal renders phases + a per-agent list (label/phase/state/tokens/
  tool-calls/duration) instead of the obsolete tool breakdown.

Sync ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md to the new
mechanism.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-20 18:14:42 +02:00
1 parent b6e546f618
commit 2b95161596
9 files changed
+453 -175

No files matched your search

+30 -29
View File
@@ -93,39 +93,35 @@ public class ClaudeCliService
}
/// <summary>
/// Greift aus einem (im Haupt-Stream unterdrückten) Subagent-Turn die Telemetrie ab:
/// die vom Subagenten aufgerufenen Tool-Namen und seine Token-Nutzung. Rein additiv für den
/// Workflow-Status – wirft nie (Telemetrie darf den Stream-Parser nicht stören).
/// Erkennt den tool_result des dynamischen Workflow-Tools ("Workflow launched in background …
/// Transcript dir: …") und registriert den Lauf im WorkflowProgressTracker. Der Workflow läuft
/// als detachter Hintergrund-Task mit eigenem Transcript – seine Live-/End-Telemetrie wird
/// später aus den dort liegenden Dateien (journal.jsonl / wf_&lt;runId&gt;.json) gelesen, nicht aus
/// dem Stream. Rein additiv – wirft nie (Telemetrie darf den Stream-Parser nicht stören).
/// </summary>
private void TapSubagentEvent(string sessionId, string chainId, string parentToolUseId, JsonElement json)
private void TapWorkflowLaunch(string sessionId, string chainId, string resultContent)
{
try
{
if (!json.TryGetProperty("message", out var msg))
if (_workflowTracker == null || string.IsNullOrEmpty(sessionId) || string.IsNullOrEmpty(resultContent))
return;
if (resultContent.IndexOf("Workflow launched", StringComparison.OrdinalIgnoreCase) < 0)
return;
var toolNames = new List<string>();
if (msg.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.Array)
// Zeile "Transcript dir: <absoluter Pfad>" extrahieren.
string transcriptDir = null;
foreach (var raw in resultContent.Split('\n'))
{
foreach (var item in content.EnumerateArray())
var line = raw.Trim();
const string marker = "Transcript dir:";
if (line.StartsWith(marker, StringComparison.OrdinalIgnoreCase))
{
if (item.TryGetProperty("type", out var it) && it.GetString() == "tool_use"
&& item.TryGetProperty("name", out var nm))
{
var n = nm.GetString();
if (!string.IsNullOrEmpty(n)) toolNames.Add(n);
}
transcriptDir = line.Substring(marker.Length).Trim();
break;
}
}
long inTok = 0, outTok = 0;
if (msg.TryGetProperty("usage", out var usage))
{
if (usage.TryGetProperty("input_tokens", out var it2) && it2.ValueKind == JsonValueKind.Number) inTok = it2.GetInt64();
if (usage.TryGetProperty("output_tokens", out var ot2) && ot2.ValueKind == JsonValueKind.Number) outTok = ot2.GetInt64();
}
_workflowTracker.RecordSubagent(sessionId, chainId, parentToolUseId, toolNames, inTok, outTok);
if (!string.IsNullOrEmpty(transcriptDir))
_workflowTracker.Register(sessionId, chainId, transcriptDir);
}
catch { /* Telemetrie darf den Stream nie stören */ }
}
@@ -553,12 +549,11 @@ public class ClaudeCliService
&& parentToolUseId.ValueKind == JsonValueKind.String
&& !string.IsNullOrEmpty(parentToolUseId.GetString()))
{
// Subagent-Event: weiterhin aus dem Haupt-Stream AUSBLENDEN (continue) –
// hier wird nur die Telemetrie für den Workflow-Status ABGEGRIFFEN
// (Tool-Aufrufe + Subagent-Tokens), ohne den sichtbaren Verlauf zu verändern.
// Die konsolidierten Subagent-Turns kommen als type:"assistant" (mit usage).
if (_workflowTracker != null && !string.IsNullOrEmpty(sessionId) && type == "assistant")
TapSubagentEvent(sessionId, chainId, parentToolUseId.GetString(), json);
// Subagent-Event aus dem Haupt-Stream AUSBLENDEN (continue). Hinweis: Die
// dynamischen Workflow-Subagenten laufen out-of-band (eigener Hintergrund-
// Task + Transcript) und tauchen hier ohnehin NICHT auf – ihre Telemetrie
// wird aus den Transcript-Dateien gelesen (siehe TapWorkflowLaunch). Dieser
// Guard betrifft nur klassische Task-Subagenten (sichtbarer Verlauf bleibt sauber).
continue;
}
@@ -775,6 +770,12 @@ public class ClaudeCliService
ToolResultContent = resultContent,
ToolResultIsError = resultIsError
});
// Dynamischen Workflow-Start abgreifen (Transcript-Pfad merken),
// damit der /workflow-status-Endpoint die Telemetrie aus den
// Transcript-Dateien lesen kann. Stört den Stream nie.
if (!resultIsError)
TapWorkflowLaunch(sessionId, chainId, resultContent);
}
}
}