Skip to content

Commit 6e12e81

Browse files
authored
release: NOVA AgentOS 1.0.1 bridge reliability
1 parent 2310b0f commit 6e12e81

1 file changed

Lines changed: 114 additions & 73 deletions

File tree

Nova.AgentOS.Bridge/Program.cs

Lines changed: 114 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text;
2+
using System.Collections.Concurrent;
23
using System.Text.Json;
34
using System.Text.Json.Nodes;
45
using System.Text.Json.Serialization;
@@ -32,13 +33,10 @@ async Task WriteProtocolAsync(object message)
3233
using var host = new AgentOsBridgeHost(
3334
(eventName, payload) => WriteProtocolAsync(new BridgeNotification(eventName, payload)));
3435

35-
while (await Console.In.ReadLineAsync() is { } line)
36-
{
37-
if (string.IsNullOrWhiteSpace(line))
38-
{
39-
continue;
40-
}
36+
var inFlightRequests = new List<Task>();
4137

38+
async Task ProcessRequestAsync(string line)
39+
{
4240
BridgeResponse response;
4341
try
4442
{
@@ -69,6 +67,24 @@ async Task WriteProtocolAsync(object message)
6967
await WriteProtocolAsync(response);
7068
}
7169

70+
while (await Console.In.ReadLineAsync() is { } line)
71+
{
72+
if (string.IsNullOrWhiteSpace(line))
73+
{
74+
continue;
75+
}
76+
77+
// A model run can legitimately stay active for many minutes. Processing
78+
// bridge messages serially made every health, recovery and start request
79+
// wait behind that run, so the Electron shell reported a false start_task
80+
// timeout. Keep protocol writes ordered through outputGate, but allow
81+
// independent AgentOS requests to progress concurrently.
82+
inFlightRequests.RemoveAll(request => request.IsCompleted);
83+
inFlightRequests.Add(ProcessRequestAsync(line));
84+
}
85+
86+
await Task.WhenAll(inFlightRequests);
87+
7288
internal sealed record BridgeRequest(string Id, string Method, JsonObject? Params);
7389
internal sealed record BridgeResponse(string Id, object? Result, BridgeError? Error);
7490
internal sealed record BridgeError(string Code, string Message);
@@ -91,8 +107,13 @@ internal sealed class AgentOsBridgeHost : IDisposable
91107
private readonly LivingMemoryService _livingMemory;
92108
private readonly EvolutionLabService _evolutionLab;
93109
private readonly RemoteCapabilityStoreService _remoteStore;
94-
private readonly Dictionary<string, TaskItem> _active =
110+
private readonly ConcurrentDictionary<string, TaskItem> _active =
111+
new(StringComparer.OrdinalIgnoreCase);
112+
private readonly ConcurrentDictionary<string, SemaphoreSlim> _startGates =
95113
new(StringComparer.OrdinalIgnoreCase);
114+
private readonly ConcurrentDictionary<string, byte> _agentRuns =
115+
new(StringComparer.OrdinalIgnoreCase);
116+
private readonly SemaphoreSlim _bootGate = new(1, 1);
96117
private bool _booted;
97118

98119
public AgentOsBridgeHost(Func<string, object, Task> publish)
@@ -147,18 +168,32 @@ public AgentOsBridgeHost(Func<string, object, Task> publish)
147168

148169
private async Task<object> BootAsync()
149170
{
150-
if (!_booted)
171+
if (_booted)
172+
{
173+
return ProjectKernel();
174+
}
175+
176+
await _bootGate.WaitAsync();
177+
try
178+
{
179+
if (!_booted)
180+
{
181+
var boot = await _kernel.BootAsync();
182+
await _supervisor.BootAsync(boot.BootId);
183+
await _kernel.ReportServiceAsync(
184+
"supervisor",
185+
"Agent Supervisor",
186+
AgentOsServiceHealth.Ready,
187+
"Electron bridge lease layer active",
188+
boot.BootId);
189+
_booted = true;
190+
}
191+
}
192+
finally
151193
{
152-
var boot = await _kernel.BootAsync();
153-
await _supervisor.BootAsync(boot.BootId);
154-
await _kernel.ReportServiceAsync(
155-
"supervisor",
156-
"Agent Supervisor",
157-
AgentOsServiceHealth.Ready,
158-
"Electron bridge lease layer active",
159-
boot.BootId);
160-
_booted = true;
194+
_bootGate.Release();
161195
}
196+
162197
return ProjectKernel();
163198
}
164199

@@ -288,6 +323,42 @@ private async Task<object> StartTaskAsync(JsonObject parameters)
288323
? parsedMode
289324
: AgentExecutionMode.Ask;
290325
var requestedTaskId = OptionalString(parameters, "taskId");
326+
var startKey = requestedTaskId ?? "new-" + Guid.NewGuid().ToString("N");
327+
var startGate = _startGates.GetOrAdd(startKey, _ => new SemaphoreSlim(1, 1));
328+
await startGate.WaitAsync();
329+
try
330+
{
331+
// A timed-out Electron call may have completed inside AgentOS and
332+
// retained the lease. Before run_agent begins, returning that same
333+
// active task is the safe idempotent response; acquiring a second
334+
// lease would incorrectly report a conflict with our own host.
335+
if (requestedTaskId is not null
336+
&& _active.TryGetValue(requestedTaskId, out var activeTask))
337+
{
338+
if (_agentRuns.ContainsKey(requestedTaskId))
339+
{
340+
throw new InvalidOperationException(
341+
$"Task {requestedTaskId} is already executing. "
342+
+ "Wait for the active run or cancel it before retrying.");
343+
}
344+
345+
return ProjectTask(activeTask);
346+
}
347+
348+
return await StartTaskCoreAsync(parameters, prompt, mode, requestedTaskId);
349+
}
350+
finally
351+
{
352+
startGate.Release();
353+
}
354+
}
355+
356+
private async Task<object> StartTaskCoreAsync(
357+
JsonObject parameters,
358+
string prompt,
359+
AgentExecutionMode mode,
360+
string? requestedTaskId)
361+
{
291362
var recovered = requestedTaskId is null
292363
? null
293364
: _snapshots.LoadAll().FirstOrDefault(item =>
@@ -419,12 +490,18 @@ await _supervisor.HeartbeatAsync(
419490
private async Task<object> RunAgentAsync(JsonObject parameters)
420491
{
421492
var task = GetActiveTask(RequiredString(parameters, "taskId"));
493+
if (!_agentRuns.TryAdd(task.Id, 0))
494+
{
495+
throw new InvalidOperationException(
496+
$"Task {task.Id} already has an active Agent run.");
497+
}
422498
var prompt = RequiredString(parameters, "prompt");
423499
var apiKey = OptionalString(parameters, "apiKey") ?? string.Empty;
424500
var endpoint = OptionalString(parameters, "endpoint");
425501
var approvalMode = OptionalString(parameters, "approvalMode") ?? "readOnly";
426502
var attachments = ParseAttachments(parameters["attachments"] as JsonArray);
427503
var conversationContext = BuildConversationContext(
504+
task.Id,
428505
parameters["conversation"] as JsonArray,
429506
prompt);
430507
task.Attachments = attachments;
@@ -591,68 +668,31 @@ await PublishEventCoreAsync(new AgentRuntimeEvent(
591668
};
592669
}
593670

594-
private static string BuildConversationContext(
671+
private string BuildConversationContext(
672+
string taskId,
595673
JsonArray? values,
596674
string currentPrompt)
597675
{
598-
if (values is null || values.Count < 2)
599-
{
600-
return string.Empty;
601-
}
602-
603-
var turns = values
676+
var turns = values?
604677
.OfType<JsonObject>()
605-
.Select(value => new
606-
{
607-
Role = OptionalString(value, "role")?.Equals(
678+
.Select((value, index) => new ConversationTurn(
679+
$"transient-{index}",
680+
taskId,
681+
OptionalString(value, "role")?.Equals(
608682
"assistant",
609683
StringComparison.OrdinalIgnoreCase) == true
610-
? "ASSISTANT"
611-
: "USER",
612-
Content = OptionalString(value, "content") ?? string.Empty
613-
})
684+
? "assistant"
685+
: "user",
686+
OptionalString(value, "content") ?? string.Empty,
687+
DateTimeOffset.UnixEpoch.AddSeconds(index)))
614688
.Where(turn => !string.IsNullOrWhiteSpace(turn.Content))
615-
.ToList();
616-
if (turns.Count > 0
617-
&& turns[^1].Role == "USER"
618-
&& turns[^1].Content.Trim().Equals(
619-
currentPrompt.Trim(),
620-
StringComparison.Ordinal))
621-
{
622-
turns.RemoveAt(turns.Count - 1);
623-
}
624-
if (turns.Count == 0)
625-
{
626-
return string.Empty;
627-
}
628-
629-
const int maximumCharacters = 48_000;
630-
var selected = new List<(string Role, string Content)>();
631-
var used = 0;
632-
for (var index = turns.Count - 1; index >= 0; index--)
633-
{
634-
var turn = turns[index];
635-
var bounded = LimitForReview(turn.Content, 12_000);
636-
if (used + bounded.Length > maximumCharacters && selected.Count > 0)
637-
{
638-
break;
639-
}
640-
selected.Add((turn.Role, bounded));
641-
used += bounded.Length;
642-
}
643-
selected.Reverse();
644-
var builder = new StringBuilder(
645-
"[NOVA CONTINUOUS CONVERSATION CONTEXT]\n"
646-
+ "以下是同一任务较早轮次的真实对话。延续已确认目标与术语;"
647-
+ "若历史与当前工作区冲突,以当前工具读取结果为准。\n");
648-
foreach (var turn in selected)
649-
{
650-
builder.AppendLine($"<{turn.Role}>");
651-
builder.AppendLine(turn.Content);
652-
builder.AppendLine($"</{turn.Role}>");
653-
}
654-
builder.AppendLine("[CURRENT USER REQUEST FOLLOWS]");
655-
return builder.ToString();
689+
.ToArray()
690+
?? [];
691+
return _conversations.BuildContextPrompt(
692+
taskId,
693+
currentPrompt,
694+
turns,
695+
includeCurrentPrompt: false);
656696
}
657697

658698
private async Task<object> VerifyResultAsync(JsonObject parameters)
@@ -879,7 +919,8 @@ await _journal.AppendAsync(
879919
}
880920
finally
881921
{
882-
_active.Remove(task.Id);
922+
_agentRuns.TryRemove(task.Id, out _);
923+
_active.TryRemove(task.Id, out _);
883924
_governor.EndTask(task.Id);
884925
}
885926
}

0 commit comments

Comments
 (0)