mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
webmcp: implement abortSignal with _dependent
This commit is contained in:
@@ -25,6 +25,7 @@ const Transfer = @import("browser/HttpClient.zig").Transfer;
|
||||
const Response = @import("browser/HttpClient.zig").Response;
|
||||
|
||||
const log = lp.log;
|
||||
const Execution = js.Execution;
|
||||
const List = std.DoublyLinkedList;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -231,7 +232,7 @@ pub const JavascriptDialogOpening = struct {
|
||||
};
|
||||
|
||||
pub const ModelContextToolEvent = struct {
|
||||
frame: *Frame,
|
||||
exec: *const Execution,
|
||||
tool: *const ModelContextTool,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,18 +24,43 @@ const js = @import("../js/js.zig");
|
||||
const Event = @import("Event.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const DOMException = @import("DOMException.zig");
|
||||
const ModelContextTool = @import("ModelContext.zig").Tool;
|
||||
|
||||
const log = lp.log;
|
||||
const Execution = js.Execution;
|
||||
|
||||
const AbortSignal = @This();
|
||||
|
||||
const Dependend = union(enum) {
|
||||
signal: *AbortSignal,
|
||||
model_context_tool: *ModelContextTool,
|
||||
|
||||
fn markAborted(self: Dependend, reason_: ?Reason, exec: *const Execution) !void {
|
||||
switch (self) {
|
||||
.signal => |dep| {
|
||||
if (dep._aborted) return;
|
||||
try dep.markAborted(reason_, exec);
|
||||
},
|
||||
.model_context_tool => |dep| {
|
||||
try dep.markAborted(exec);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatchAbortEvent(self: Dependend, exec: *const Execution) !void {
|
||||
switch (self) {
|
||||
.signal => |dep| try dep.dispatchAbortEvent(exec),
|
||||
.model_context_tool => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_proto: *EventTarget,
|
||||
_aborted: bool = false,
|
||||
_is_dependent: bool = false,
|
||||
_reason: Reason = .undefined,
|
||||
_on_abort: ?js.Function.Global = null,
|
||||
_dependents: std.ArrayList(*AbortSignal) = .{},
|
||||
_dependents: std.ArrayList(Dependend) = .{},
|
||||
_source_signals: std.ArrayList(*AbortSignal) = .{},
|
||||
|
||||
pub fn init(exec: *const Execution) !*AbortSignal {
|
||||
@@ -74,9 +99,8 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void
|
||||
// Per spec: mark all direct dependents aborted (with this signal's reason)
|
||||
// BEFORE firing any abort events. The graph is flattened at any() creation,
|
||||
// so we never need to recurse here.
|
||||
var to_dispatch: std.ArrayList(*AbortSignal) = .{};
|
||||
var to_dispatch: std.ArrayList(Dependend) = .{};
|
||||
for (self._dependents.items) |dep| {
|
||||
if (dep._aborted) continue;
|
||||
try dep.markAborted(self._reason, exec);
|
||||
try to_dispatch.append(exec.arena, dep);
|
||||
}
|
||||
@@ -136,11 +160,11 @@ pub fn createAny(signals: []const *AbortSignal, exec: *const Execution) !*AbortS
|
||||
|
||||
for (signals) |source| {
|
||||
if (!source._is_dependent) {
|
||||
try source._dependents.append(exec.arena, result);
|
||||
try source._dependents.append(exec.arena, .{ .signal = result });
|
||||
try result._source_signals.append(exec.arena, source);
|
||||
} else {
|
||||
for (source._source_signals.items) |s| {
|
||||
try s._dependents.append(exec.arena, result);
|
||||
try s._dependents.append(exec.arena, .{ .signal = result });
|
||||
try result._source_signals.append(exec.arena, s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Notification = @import("../../Notification.zig");
|
||||
|
||||
const AbortSignal = @import("AbortSignal.zig");
|
||||
const Execution = js.Execution;
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{ ModelContext, ModelContextClient };
|
||||
@@ -56,6 +56,7 @@ pub const Annotations = struct {
|
||||
};
|
||||
|
||||
pub const Tool = struct {
|
||||
ctx: *ModelContext,
|
||||
name: []const u8,
|
||||
title: ?[]const u8,
|
||||
description: []const u8,
|
||||
@@ -66,6 +67,10 @@ pub const Tool = struct {
|
||||
// fires. Checked lazily on each `tools()` / `findTool()` call — fine
|
||||
// for headless usage where there's no synchronous observer to notify.
|
||||
signal: ?*AbortSignal,
|
||||
|
||||
pub fn markAborted(self: *Tool, exec: *const Execution) !void {
|
||||
try self.ctx.markAborted(self, exec);
|
||||
}
|
||||
};
|
||||
|
||||
const ToolDict = struct {
|
||||
@@ -85,7 +90,7 @@ pub fn registerTool(
|
||||
self: *ModelContext,
|
||||
tool: ToolDict,
|
||||
options_: ?RegisterToolOptions,
|
||||
frame: *Frame,
|
||||
exec: *const Execution,
|
||||
) !void {
|
||||
try validateName(tool.name);
|
||||
if (tool.description.len == 0) {
|
||||
@@ -104,16 +109,16 @@ pub fn registerTool(
|
||||
// Reject duplicate names. The spec says `InvalidStateError`. We compact
|
||||
// the list lazily here so a tool whose signal already aborted doesn't
|
||||
// block re-registering under the same name.
|
||||
self.compactAborted(frame);
|
||||
for (self._tools.items) |existing| {
|
||||
if (std.mem.eql(u8, existing.name, tool.name)) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
}
|
||||
|
||||
const arena = frame.arena;
|
||||
const arena = exec.arena;
|
||||
const entry = try arena.create(Tool);
|
||||
entry.* = .{
|
||||
.ctx = self,
|
||||
.name = try arena.dupe(u8, tool.name),
|
||||
.title = if (tool.title) |t| try arena.dupe(u8, t) else null,
|
||||
.description = try arena.dupe(u8, tool.description),
|
||||
@@ -123,25 +128,31 @@ pub fn registerTool(
|
||||
.signal = options.signal,
|
||||
};
|
||||
|
||||
if (entry.signal) |s| {
|
||||
try s._dependents.append(arena, .{ .model_context_tool = entry });
|
||||
}
|
||||
try self._tools.append(arena, entry);
|
||||
|
||||
// Fire `model_context_tool_added` so observers (CDP `WebMCP` domain,
|
||||
// native MCP forwarder) can surface the new tool.
|
||||
const event: Notification.ModelContextToolEvent = .{ .frame = frame, .tool = entry };
|
||||
frame._session.notification.dispatch(.model_context_tool_added, &event);
|
||||
const event: Notification.ModelContextToolEvent = .{ .exec = exec, .tool = entry };
|
||||
|
||||
const session = switch (exec.context.global) {
|
||||
inline else => |g| g._session,
|
||||
};
|
||||
|
||||
session.notification.dispatch(.model_context_tool_added, &event);
|
||||
}
|
||||
|
||||
/// Snapshot of currently-registered tools, with aborted entries filtered.
|
||||
/// Used by the CDP `WebMCP.enable` replay and the native MCP forwarder.
|
||||
pub fn tools(self: *ModelContext, frame: *Frame) []const *Tool {
|
||||
self.compactAborted(frame);
|
||||
pub fn tools(self: *ModelContext) []const *Tool {
|
||||
return self._tools.items;
|
||||
}
|
||||
|
||||
/// Look up a tool by name. Returns null if not found or if its signal has
|
||||
/// fired. Used by CDP `WebMCP.invokeTool`.
|
||||
pub fn findTool(self: *ModelContext, frame: *Frame, name: []const u8) ?*Tool {
|
||||
self.compactAborted(frame);
|
||||
pub fn findTool(self: *ModelContext, name: []const u8) ?*Tool {
|
||||
for (self._tools.items) |t| {
|
||||
if (std.mem.eql(u8, t.name, name)) return t;
|
||||
}
|
||||
@@ -151,17 +162,19 @@ pub fn findTool(self: *ModelContext, frame: *Frame, name: []const u8) ?*Tool {
|
||||
/// Walk the tool list and remove any whose `AbortSignal` has fired,
|
||||
/// dispatching `model_context_tool_removed` for each. Cheap when no
|
||||
/// signals fired (which is the common case).
|
||||
fn compactAborted(self: *ModelContext, frame: *Frame) void {
|
||||
fn markAborted(self: *ModelContext, tool: *Tool, exec: *const Execution) !void {
|
||||
const session = switch (exec.context.global) {
|
||||
inline else => |g| g._session,
|
||||
};
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < self._tools.items.len) {
|
||||
const t = self._tools.items[i];
|
||||
if (t.signal) |signal| {
|
||||
if (signal._aborted) {
|
||||
_ = self._tools.swapRemove(i);
|
||||
const event: Notification.ModelContextToolEvent = .{ .frame = frame, .tool = t };
|
||||
frame._session.notification.dispatch(.model_context_tool_removed, &event);
|
||||
continue;
|
||||
}
|
||||
if (t == tool) {
|
||||
_ = self._tools.swapRemove(i);
|
||||
const event: Notification.ModelContextToolEvent = .{ .exec = exec, .tool = t };
|
||||
session.notification.dispatch(.model_context_tool_removed, &event);
|
||||
return;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
@@ -191,10 +204,12 @@ pub const ModelContextClient = struct {
|
||||
pub fn requestUserInteraction(
|
||||
_: *ModelContextClient,
|
||||
callback: js.Function,
|
||||
frame: *Frame,
|
||||
exec: *const Execution,
|
||||
) !js.Promise {
|
||||
const local = frame.js.local.?;
|
||||
const resolver = local.createPromiseResolver();
|
||||
var ls: js.Local.Scope = undefined;
|
||||
exec.context.global.getJs().localScope(&ls);
|
||||
defer ls.deinit();
|
||||
const resolver = ls.local.createPromiseResolver();
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
if (callback.tryCall(js.Value, .{}, &caught)) |result| {
|
||||
|
||||
@@ -65,7 +65,7 @@ fn enable(cmd: *CDP.Command) !void {
|
||||
// frame only; subframes will be added when they register.
|
||||
if (bc.session.currentFrame()) |frame| {
|
||||
const mc = frame.window.getModelContext();
|
||||
const tools = mc.tools(frame);
|
||||
const tools = mc.tools();
|
||||
if (tools.len > 0) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
@@ -102,7 +102,7 @@ fn invokeTool(cmd: *CDP.Command) !void {
|
||||
const frame_id = try id.parseFrameId(params.frameId);
|
||||
const frame = bc.session.findFrameByFrameId(frame_id) orelse return error.FrameNotFound;
|
||||
const mc = frame.window.getModelContext();
|
||||
const tool = mc.findTool(frame, params.toolName) orelse return error.NotFound;
|
||||
const tool = mc.findTool(params.toolName) orelse return error.NotFound;
|
||||
|
||||
// Stringify the input once. We send it back to the client via
|
||||
// `toolInvoked.input` and pass the parsed form into the JS callback.
|
||||
@@ -256,12 +256,18 @@ pub fn onToolAdded(
|
||||
bc: *CDP.BrowserContext,
|
||||
event: *const Notification.ModelContextToolEvent,
|
||||
) !void {
|
||||
const global = event.exec.context.global;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
event.frame.js.localScope(&ls);
|
||||
global.getJs().localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
const frame_id = switch (global) {
|
||||
inline else => |g| g._frame_id,
|
||||
};
|
||||
|
||||
const writer = ToolWriter{
|
||||
.frame_id = id.toFrameId(event.frame._frame_id),
|
||||
.frame_id = id.toFrameId(frame_id),
|
||||
.tools = &.{event.tool},
|
||||
.local = &ls.local,
|
||||
};
|
||||
@@ -274,9 +280,12 @@ pub fn onToolRemoved(
|
||||
bc: *CDP.BrowserContext,
|
||||
event: *const Notification.ModelContextToolEvent,
|
||||
) !void {
|
||||
const frame_id = switch (event.exec.context.global) {
|
||||
inline else => |g| g._frame_id,
|
||||
};
|
||||
try bc.cdp.sendEvent("WebMCP.toolsRemoved", .{
|
||||
.tools = &.{
|
||||
.{ .name = event.tool.name, .frameId = id.toFrameId(event.frame._frame_id) },
|
||||
.{ .name = event.tool.name, .frameId = id.toFrameId(frame_id) },
|
||||
},
|
||||
}, .{ .session_id = bc.session_id });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user