Automatisierungsvorschlaege: Agenten schlagen n8n-Workflows vor, der Mensch importiert von Hand (#726)
- Core/Automation/WorkflowReview: free check of an n8n export - every node with its effect and target, code/command/SSH nodes only with a stated reason, embedded secrets refused (by name or by look), a short summary of what the workflow does. - AutomationProposals: measured saving from the replaced tasks' receipts (28 days), at most 3 open per agent, the same purpose not twice, a rejected proposal must be answered; the human gets a task and decides in the approvals view. Accepting records it - importing stays a human's step. - Candidates: recurring tasks whose runs repeat the same calls (>= 75 % of >= 8 runs in 14 days) are put to their agent once a month. - Escalation tool: automation_find, automation_propose. - Approvals view: proposals with nodes, targets, warnings, saving, JSON download, accept/reject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
64e09b82d5
commit
067ff62317
@@ -41,6 +41,7 @@ public sealed class EscalationTool : IAgentTool
|
||||
spot_checks lists the runs picked for you, with what the audit log shows; inspect_run (run) shows one run in full;
|
||||
check_done (check, verdict ok|finding, note) closes a check. propose_measure (agent, measure, reasoning, check) puts a measure to a human:
|
||||
sharpen_task, change_model, adjust_rights, suspend — or security, if you suspect the agent is steered from outside.
|
||||
Routine you do the same way again and again may not need a model: automation_find (is there a proposal for it, and why was one rejected?), then automation_propose (purpose, trigger, tasks it replaces, services, risks, workflow as n8n JSON, test_guide; justification if it runs code). A human decides and imports it — you never create workflows yourself.
|
||||
As a voter: list_ballots shows the calls you are asked to vote on; cast_vote (vote, choice yes|no, reasoning, evidence) — check against your own data, with source and time. You never see the other votes.
|
||||
""";
|
||||
|
||||
@@ -50,7 +51,7 @@ public sealed class EscalationTool : IAgentTool
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["find_decisions", "escalate", "list_cases", "decide", "handle", "forward", "open_conflict", "state_position", "request_budget", "list_approvals", "approve_call", "reject_call", "doctrine_read", "doctrine_set", "spot_checks", "inspect_run", "check_done", "propose_measure", "list_ballots", "cast_vote"]
|
||||
"enum": ["find_decisions", "escalate", "list_cases", "decide", "handle", "forward", "open_conflict", "state_position", "request_budget", "list_approvals", "approve_call", "reject_call", "doctrine_read", "doctrine_set", "spot_checks", "inspect_run", "check_done", "propose_measure", "list_ballots", "cast_vote", "automation_find", "automation_propose"]
|
||||
},
|
||||
"amount_usd": { "type": "number", "description": "request_budget: the amount for the task and all its parts, in USD." },
|
||||
"subject": { "type": "string", "description": "What it is about, in a few words — like a title. escalate, open_conflict, find_decisions." },
|
||||
@@ -76,7 +77,17 @@ public sealed class EscalationTool : IAgentTool
|
||||
"measure": { "type": "string", "enum": ["sharpen_task", "change_model", "adjust_rights", "suspend", "security"], "description": "propose_measure: what should follow." },
|
||||
"vote": { "type": "integer", "description": "cast_vote: the vote number (#…)." },
|
||||
"choice": { "type": "string", "enum": ["yes", "no"], "description": "cast_vote: your vote." },
|
||||
"evidence": { "type": "string", "description": "cast_vote: what you checked — source and time. Required: without it a vote is an opinion." }
|
||||
"evidence": { "type": "string", "description": "cast_vote: what you checked — source and time. Required: without it a vote is an opinion." },
|
||||
"purpose": { "type": "string", "description": "automation_propose: what the workflow is for. automation_find: what to look for." },
|
||||
"trigger": { "type": "string", "description": "automation_propose: what starts it (mail, schedule, webhook …) and how often it happens." },
|
||||
"tasks": { "type": "array", "items": { "type": "string" }, "description": "automation_propose: the task ids it replaces — the saving is measured from their runs." },
|
||||
"services": { "type": "array", "items": { "type": "string" }, "description": "automation_propose: the services and credentials it needs — names only, never values." },
|
||||
"risks": { "type": "string", "description": "automation_propose: what can go wrong, and what it sends outside." },
|
||||
"workflow": { "description": "automation_propose: the n8n workflow as exported JSON ({ nodes, connections }) — object or string. Credentials only as references." },
|
||||
"test_guide": { "type": "string", "description": "automation_propose: how the human checks it after importing." },
|
||||
"justification": { "type": "string", "description": "automation_propose: why a Code, Execute Command or SSH node is needed. Without it such nodes are refused." },
|
||||
"answers": { "type": "integer", "description": "automation_propose: the rejected proposal (#) this one answers." },
|
||||
"argument": { "type": "string", "description": "automation_propose: what is new against that rejection's reason." }
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
@@ -108,6 +119,10 @@ public sealed class EscalationTool : IAgentTool
|
||||
if (Get(input, "action") is "spot_checks" or "inspect_run" or "check_done" or "propose_measure")
|
||||
return await SpotCheckAsync(input, context, ct);
|
||||
|
||||
// #726: Proposing a workflow instead of doing routine by model.
|
||||
if (Get(input, "action") is "automation_find" or "automation_propose")
|
||||
return await AutomationAsync(input, context, ct);
|
||||
|
||||
// #702: A voter's ballot.
|
||||
if (Get(input, "action") is "list_ballots" or "cast_vote")
|
||||
return await BallotAsync(input, context, ct);
|
||||
@@ -184,7 +199,7 @@ public sealed class EscalationTool : IAgentTool
|
||||
break;
|
||||
|
||||
default:
|
||||
return ToolResult.Fail("'action' must be one of find_decisions, escalate, list_cases, decide, handle, forward, open_conflict, state_position, request_budget, list_approvals, approve_call, reject_call, doctrine_read, doctrine_set, spot_checks, inspect_run, check_done, propose_measure, list_ballots, cast_vote.");
|
||||
return ToolResult.Fail("'action' must be one of find_decisions, escalate, list_cases, decide, handle, forward, open_conflict, state_position, request_budget, list_approvals, approve_call, reject_call, doctrine_read, doctrine_set, spot_checks, inspect_run, check_done, propose_measure, list_ballots, cast_vote, automation_find, automation_propose.");
|
||||
}
|
||||
|
||||
return outcome.Ok ? ToolResult.Ok(outcome.Message) : ToolResult.Fail(outcome.Message);
|
||||
@@ -336,6 +351,41 @@ public sealed class EscalationTool : IAgentTool
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<ToolResult> AutomationAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.Automations is not { } automations)
|
||||
return ToolResult.Fail("Automation proposals are not available in this instance.");
|
||||
|
||||
if (Get(input, "action") == "automation_find")
|
||||
{
|
||||
var subject = Get(input, "purpose") ?? Get(input, "subject") ?? "";
|
||||
if (string.IsNullOrWhiteSpace(subject))
|
||||
return ToolResult.Fail("'purpose' is required — what to look for.");
|
||||
var found = await automations.FindAsync(subject, ct);
|
||||
if (found.Count == 0)
|
||||
return ToolResult.Ok($"No proposal is about \"{subject.Trim()}\".");
|
||||
var sb = new StringBuilder("Proposals about it:\n");
|
||||
foreach (var p in found)
|
||||
sb.AppendLine($"- #{p.Id} by {p.AgentId}: {p.Purpose} — {p.Status}" + (string.IsNullOrWhiteSpace(p.Reason) ? "" : $" (reason: {p.Reason})"));
|
||||
return ToolResult.Ok(sb.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
var workflow = input.TryGetProperty("workflow", out var w)
|
||||
? w.ValueKind == JsonValueKind.String ? w.GetString() ?? "" : w.GetRawText()
|
||||
: "";
|
||||
long? answers = input.TryGetProperty("answers", out var a) && a.ValueKind == JsonValueKind.Number && a.TryGetInt64(out var n) ? n : null;
|
||||
var outcome = await automations.ProposeAsync(new Core.Automation.AutomationRequest(
|
||||
context.AgentId, Get(input, "purpose") ?? "", Get(input, "trigger") ?? "",
|
||||
List(input, "tasks"), List(input, "services"), Get(input, "risks") ?? "", Get(input, "test_guide") ?? "",
|
||||
workflow, Get(input, "justification"), answers, Get(input, "argument")), ct);
|
||||
return outcome.Ok ? ToolResult.Ok(outcome.Message) : ToolResult.Fail(outcome.Message);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> List(JsonElement input, string name)
|
||||
=> input.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Array
|
||||
? v.EnumerateArray().Where(e => e.ValueKind == JsonValueKind.String).Select(e => e.GetString()!.Trim()).Where(s => s.Length > 0).ToList()
|
||||
: [];
|
||||
|
||||
private static async Task<ToolResult> BallotAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.Votes is not { } votes)
|
||||
|
||||
Reference in New Issue
Block a user