feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Nexus.Api.Data;
|
||||
using Nexus.Api.Models;
|
||||
|
||||
namespace Nexus.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Small capability-gated adapter around the protocol connector. Keeping
|
||||
/// invocation metadata at this seam lets the connector add W3C trace context
|
||||
/// without leaking Gateway protocol details into the run domain service.
|
||||
/// </summary>
|
||||
public sealed class OpenClawRunGateway(
|
||||
IGatewayConnector connector,
|
||||
IOpenClawWriteGate writeGate) : IOpenClawRunGateway
|
||||
{
|
||||
public async Task<OpenClawRunGatewayResult> StartAsync(
|
||||
OpenClawRun run,
|
||||
OpenClawInvocationMetadata invocation,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var writeDecision = await writeGate.EvaluateAsync(
|
||||
"chat.send",
|
||||
"operator.write",
|
||||
cancellationToken);
|
||||
if (!writeDecision.Allowed)
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
false,
|
||||
false,
|
||||
OpenClawRunStates.Blocked,
|
||||
writeDecision.Recovery is null
|
||||
? writeDecision.Message
|
||||
: $"{writeDecision.Message} {writeDecision.Recovery}");
|
||||
}
|
||||
|
||||
if (connector.ConnectionState != GatewayConnectionState.Connected)
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
false,
|
||||
OpenClawRunStates.Blocked,
|
||||
"OpenClaw Gateway is not connected. The run remains durable in Nexus and can be retried.");
|
||||
}
|
||||
|
||||
if (!connector.Supports("chat.send"))
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
false,
|
||||
false,
|
||||
OpenClawRunStates.Unsupported,
|
||||
"The connected OpenClaw Gateway does not advertise chat.send.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await connector.InvokeAsync(
|
||||
"chat.send",
|
||||
new
|
||||
{
|
||||
sessionKey = run.SessionKey,
|
||||
agentId = run.AgentId,
|
||||
message = run.Prompt,
|
||||
deliver = false,
|
||||
idempotencyKey = invocation.IdempotencyKey
|
||||
},
|
||||
cancellationToken: cancellationToken,
|
||||
invocationContext: ToGatewayContext(invocation, includeIdempotencyParameter: true));
|
||||
|
||||
var runId = ReadString(response?["runId"]);
|
||||
var gatewayStatus = ReadString(response?["status"])?.ToLowerInvariant();
|
||||
var state = gatewayStatus switch
|
||||
{
|
||||
"ok" => OpenClawRunStates.Completed,
|
||||
"started" or "in_flight" => OpenClawRunStates.Running,
|
||||
_ => OpenClawRunStates.Running
|
||||
};
|
||||
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
true,
|
||||
state,
|
||||
gatewayStatus is null
|
||||
? "OpenClaw accepted the run."
|
||||
: $"OpenClaw acknowledged the run as '{gatewayStatus}'.",
|
||||
runId,
|
||||
OpenClawPayloadSanitizer.Redact(response));
|
||||
}
|
||||
catch (OpenClawGatewayRpcException exception)
|
||||
{
|
||||
return FromException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OpenClawRunGatewayResult> StopAsync(
|
||||
OpenClawRun run,
|
||||
OpenClawInvocationMetadata invocation,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (connector.ConnectionState != GatewayConnectionState.Connected)
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
false,
|
||||
OpenClawRunStates.Blocked,
|
||||
"OpenClaw Gateway is not connected; Nexus did not claim the run was stopped.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(run.OpenClawRunId))
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
false,
|
||||
OpenClawRunStates.Blocked,
|
||||
"The run has no exact OpenClaw run id. Nexus will not abort every run in the session.");
|
||||
}
|
||||
|
||||
if (!connector.Supports("chat.abort")
|
||||
&& !connector.Supports("sessions.abort"))
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
false,
|
||||
false,
|
||||
OpenClawRunStates.Unsupported,
|
||||
"The connected OpenClaw Gateway advertises neither chat.abort nor sessions.abort.");
|
||||
}
|
||||
|
||||
var abortMethod = connector.Supports("chat.abort")
|
||||
? "chat.abort"
|
||||
: "sessions.abort";
|
||||
var writeDecision = await writeGate.EvaluateAsync(
|
||||
abortMethod,
|
||||
"operator.write",
|
||||
cancellationToken);
|
||||
if (!writeDecision.Allowed)
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
false,
|
||||
false,
|
||||
OpenClawRunStates.Blocked,
|
||||
writeDecision.Recovery is null
|
||||
? writeDecision.Message
|
||||
: $"{writeDecision.Message} {writeDecision.Recovery}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
JsonNode? response;
|
||||
if (connector.Supports("chat.abort"))
|
||||
{
|
||||
response = await connector.InvokeAsync(
|
||||
"chat.abort",
|
||||
new
|
||||
{
|
||||
sessionKey = run.SessionKey,
|
||||
agentId = run.AgentId,
|
||||
runId = run.OpenClawRunId
|
||||
},
|
||||
cancellationToken: cancellationToken,
|
||||
invocationContext: ToGatewayContext(invocation));
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await connector.InvokeAsync(
|
||||
"sessions.abort",
|
||||
new
|
||||
{
|
||||
key = run.SessionKey,
|
||||
runId = run.OpenClawRunId,
|
||||
clearQueued = false
|
||||
},
|
||||
cancellationToken: cancellationToken,
|
||||
invocationContext: ToGatewayContext(invocation));
|
||||
}
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
true,
|
||||
OpenClawRunStates.Stopped,
|
||||
"OpenClaw accepted the exact run abort.",
|
||||
run.OpenClawRunId,
|
||||
OpenClawPayloadSanitizer.Redact(response));
|
||||
}
|
||||
catch (OpenClawGatewayRpcException exception)
|
||||
{
|
||||
return FromException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OpenClawRunGatewayResult> GetHistoryAsync(
|
||||
OpenClawRun run,
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (connector.ConnectionState != GatewayConnectionState.Connected)
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
false,
|
||||
OpenClawRunStates.Blocked,
|
||||
"OpenClaw Gateway is disconnected; durable Nexus transitions are still available.");
|
||||
}
|
||||
|
||||
if (!connector.Supports("chat.history"))
|
||||
{
|
||||
return new OpenClawRunGatewayResult(
|
||||
false,
|
||||
false,
|
||||
OpenClawRunStates.Unsupported,
|
||||
"The connected OpenClaw Gateway does not advertise chat.history.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await connector.InvokeAsync(
|
||||
"chat.history",
|
||||
new
|
||||
{
|
||||
sessionKey = run.SessionKey,
|
||||
agentId = run.AgentId,
|
||||
limit = Math.Clamp(limit, 1, 1000),
|
||||
maxChars = 200_000
|
||||
},
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
true,
|
||||
"available",
|
||||
"OpenClaw returned display-normalized session history.",
|
||||
run.OpenClawRunId,
|
||||
OpenClawPayloadSanitizer.Redact(response));
|
||||
}
|
||||
catch (OpenClawGatewayRpcException exception)
|
||||
{
|
||||
return FromException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenClawRunGatewayResult FromException(OpenClawGatewayRpcException exception)
|
||||
{
|
||||
var state = exception.Code is "GATEWAY_DISCONNECTED" or "UNAVAILABLE"
|
||||
? OpenClawRunStates.Blocked
|
||||
: OpenClawRunStates.Failed;
|
||||
return new OpenClawRunGatewayResult(
|
||||
true,
|
||||
false,
|
||||
state,
|
||||
$"OpenClaw rejected the operation ({exception.Code}).");
|
||||
}
|
||||
|
||||
private static OpenClawInvocationContext ToGatewayContext(
|
||||
OpenClawInvocationMetadata invocation,
|
||||
bool includeIdempotencyParameter = false)
|
||||
=> OpenClawInvocationContext.Create(
|
||||
invocation.Actor,
|
||||
invocation.IdempotencyKey,
|
||||
invocation.CorrelationId,
|
||||
invocation.TraceParent,
|
||||
includeIdempotencyParameter);
|
||||
|
||||
private static string? ReadString(JsonNode? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value?.GetValue<string>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value?.ToJsonString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user