Verweis statt Nacherzaehlung: die Engine stempelt den Ursprung (#697, #677)

- Werkzeugkontext kennt Lauf und Aufgabe des Laufs (RunId, RunTaskId); die
  Delegationskette traegt den Lauf des Aufrufers weiter (RunId, CallerRunId)
- Aufgaben tragen origin_run / origin_task (Datei, DB, Taskboard "Arose from"),
  gestempelt, nie vom Agenten behauptet
- Kommentare nennen den Lauf, in dem sie geschrieben wurden
- Laufbeleg: CallerRunId; Laufansicht fuehrt zu "Delegiert von" und "Delegiert an"
- #677: task_update ersetzt den Rumpf nicht mehr, Abnahmekriterien aendert nur der
  Ersteller; Kommentare der Fremdinhalt-Stufe auf fremden Aufgaben kommen gerahmt an
- WebUI: Herkunft auf der Aufgabenkarte mit Link zum Lauf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-25 09:27:47 +02:00
co-authored by Claude Opus 5.5
parent 3f90e0d0cd
commit f812dbd856
26 changed files with 493 additions and 47 deletions
+16 -9
View File
@@ -203,7 +203,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
var result = await RunCoreAsync(agentConfig, userMessage, instanceId, externalCt, runId, source, chain, recorder);
activity.Complete(result.Status.ToString());
await RecordUsageAsync(agentConfig, result);
await RecordReceiptAsync(runId, agentConfig, result, source ?? AuditSource.Direct, taskId);
await RecordReceiptAsync(runId, agentConfig, result, source ?? AuditSource.Direct, taskId, chain.CallerRunId);
await SaveTranscriptAsync(recorder, runId, agentConfig, result, source ?? AuditSource.Direct, taskId);
return result;
}
@@ -526,7 +526,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
var result = await ChatCoreAsync(agentConfig, userMessage, instanceId, runCts.Token, source, runId, chain, recorder);
activity.Complete(result.Status.ToString());
await RecordUsageAsync(agentConfig, result);
await RecordReceiptAsync(runId, agentConfig, result, source, taskId);
await RecordReceiptAsync(runId, agentConfig, result, source, taskId, chain.CallerRunId);
await SaveTranscriptAsync(recorder, runId, agentConfig, result, source ?? AuditSource.Direct, taskId);
return result;
}
@@ -1501,7 +1501,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
? default
: JsonDocument.Parse(arguments).RootElement;
var context = BuildToolContext(agentConfig, instanceId, toolName, ct, chain);
var context = BuildToolContext(agentConfig, instanceId, toolName, ct, chain, runId);
// #579: Was das Werkzeug ohnehin verbieten wuerde, kommt gar nicht erst in die
// Freigabeliste. Vorher lag dort z.B. ein Loeschvorschlag eines Agenten, der
@@ -1660,13 +1660,14 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
private AgentToolContext BuildToolContext(
AgentConfig agentConfig, string instanceId, string toolName, CancellationToken ct,
DelegationChain? chain = null)
DelegationChain? chain = null, string? runId = null)
{
var toolConfig = agentConfig.Tools.TryGetValue(toolName, out var cfg)
? cfg.AsReadOnly()
: new Dictionary<string, object?>().AsReadOnly();
var toolLogger = _loggerFactory.CreateLogger($"ClawdDotNet.Tools.{toolName}.Execution");
var baseChain = chain ?? DelegationChain.Root(agentConfig.AgentId);
return new AgentToolContext(
agentConfig.AgentId,
@@ -1680,10 +1681,14 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
this,
_memoryRepository,
_taskRepository,
chain ?? DelegationChain.Root(agentConfig.AgentId))
// #697: The chain carries the caller's run, so a delegated run can name where it came from.
runId is null ? baseChain : baseChain.WithRun(runId))
{
Organisation = CurrentOrganisation(),
Trust = ReportingLine.TrustOf(agentConfig, _toolRegistry.Get)
Trust = ReportingLine.TrustOf(agentConfig, _toolRegistry.Get),
// #697: Where something an agent hands on came from — known to the engine, not claimed by the agent.
RunId = runId,
RunTaskId = runId is null ? null : Activity.Find(runId)?.TaskId
};
}
@@ -1712,7 +1717,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
: JsonDocument.Parse(argumentsJson).RootElement;
// #641: Ein Mensch hat genau diesen Aufruf freigegeben.
var context = BuildToolContext(config, _instanceId, tool, ct) with { FreigabeGeprueft = true };
var context = BuildToolContext(config, _instanceId, tool, ct, runId: runId) with { FreigabeGeprueft = true };
var result = await agentTool.ExecuteAsync(input, context, ct);
var status = result.Success ? AuditStatus.Ok : AuditStatus.Error;
@@ -1907,7 +1912,8 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
}
private async Task RecordReceiptAsync(
string runId, AgentConfig agentConfig, AgentRunResult result, string? source, string? taskId)
string runId, AgentConfig agentConfig, AgentRunResult result, string? source, string? taskId,
string? callerRunId = null)
{
if (_auditRepository is null)
return;
@@ -1937,7 +1943,8 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
CostIsKnown = reported is not null || (estimate?.IsKnown ?? false),
DurationMs = (long)result.Duration.TotalMilliseconds,
ResultRef = Cap(result.FinalMessage ?? "", 500),
OccurredAt = DateTime.Now
OccurredAt = DateTime.Now,
CallerRunId = callerRunId
}, CancellationToken.None);
}
catch (Exception ex)