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:
1 parent
b6e546f618
commit
2b95161596
9 files changed
+453
-175
No files matched your search
@@ -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_<runId>.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user