Add compact agent context command
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
const std = @import("std");
|
||||
const session_mod = @import("session.zig");
|
||||
|
||||
// Compact local context formatting for same-user tools/agents.
|
||||
// req: session/002, governance/002, governance/003, testing/001, testing/002
|
||||
|
||||
test {
|
||||
_ = sessionContextAlloc;
|
||||
}
|
||||
|
||||
pub const Limits = struct {
|
||||
max_bytes_preview: usize = 48,
|
||||
max_panel_summary: usize = 96,
|
||||
max_task: usize = 96,
|
||||
};
|
||||
|
||||
pub fn localContextAlloc(allocator: std.mem.Allocator, cwd: []const u8, task: ?[]const u8, intent: ?[]const u8, limits: Limits) ![]u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
try appendLine(allocator, &out, "context:v1");
|
||||
try appendField(allocator, &out, "cwd", cwd, limits.max_panel_summary);
|
||||
if (task) |value| try appendField(allocator, &out, "task", value, limits.max_task);
|
||||
if (intent) |value| try appendField(allocator, &out, "intent", value, limits.max_task);
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
pub fn sessionContextAlloc(allocator: std.mem.Allocator, session: *const session_mod.Session, task: ?[]const u8, intent: ?[]const u8, limits: Limits) ![]u8 {
|
||||
const snap = try session.snapshot();
|
||||
const panel_path = try session.panelPathAlloc(allocator);
|
||||
defer allocator.free(panel_path);
|
||||
const panel_summary = try session.panelSummaryAlloc(allocator);
|
||||
defer allocator.free(panel_summary);
|
||||
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
try appendLine(allocator, &out, "context:v1");
|
||||
try appendNumber(allocator, &out, "cursor_byte", snap.cursor_byte);
|
||||
try appendNumber(allocator, &out, "cursor_cell", snap.cursor_cell);
|
||||
try appendNumber(allocator, &out, "bytes_len", snap.bytes.len);
|
||||
try appendNumber(allocator, &out, "panel_depth", snap.panel_depth);
|
||||
try appendField(allocator, &out, "active_panel", if (snap.active_panel_title) |title| title else "-", limits.max_panel_summary);
|
||||
try appendField(allocator, &out, "panel_path", panel_path, limits.max_panel_summary);
|
||||
try appendField(allocator, &out, "panel_summary", panel_summary, limits.max_panel_summary);
|
||||
try appendField(allocator, &out, "buffer_preview", snap.bytes[0..@min(snap.bytes.len, limits.max_bytes_preview)], limits.max_bytes_preview);
|
||||
if (task) |value| try appendField(allocator, &out, "task", value, limits.max_task);
|
||||
if (intent) |value| try appendField(allocator, &out, "intent", value, limits.max_task);
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn appendNumber(allocator: std.mem.Allocator, out: *std.ArrayList(u8), key: []const u8, value: usize) !void {
|
||||
const line = try std.fmt.allocPrint(allocator, "{s}={d}", .{ key, value });
|
||||
defer allocator.free(line);
|
||||
try appendLine(allocator, out, line);
|
||||
}
|
||||
|
||||
fn appendField(allocator: std.mem.Allocator, out: *std.ArrayList(u8), key: []const u8, value: []const u8, max_len: usize) !void {
|
||||
const clean = try sanitizeAlloc(allocator, value, max_len);
|
||||
defer allocator.free(clean);
|
||||
const line = try std.fmt.allocPrint(allocator, "{s}={s}", .{ key, clean });
|
||||
defer allocator.free(line);
|
||||
try appendLine(allocator, out, line);
|
||||
}
|
||||
|
||||
fn appendLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line: []const u8) !void {
|
||||
try out.appendSlice(allocator, line);
|
||||
try out.append(allocator, '\n');
|
||||
}
|
||||
|
||||
fn sanitizeAlloc(allocator: std.mem.Allocator, value: []const u8, max_len: usize) ![]u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
var i: usize = 0;
|
||||
while (i < value.len and out.items.len < max_len) {
|
||||
const len = std.unicode.utf8ByteSequenceLength(value[i]) catch 1;
|
||||
const end = @min(value.len, i + len);
|
||||
const slice = value[i..end];
|
||||
if (slice.len == 1 and (slice[0] <= 0x20 or slice[0] == '|')) {
|
||||
try out.append(allocator, '_');
|
||||
} else {
|
||||
try out.appendSlice(allocator, slice);
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
if (i < value.len) try out.appendSlice(allocator, "...");
|
||||
if (out.items.len == 0) try out.append(allocator, '-');
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
test "regular: session context includes compact editor and panel state" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
try session.openFixture("pub const x = 1;\n");
|
||||
try session.openListPanel("diagnostics", &.{ "lsp:diagnostics:count_1", "lsp:diag:error:1:5:file:bad" });
|
||||
|
||||
const text = try sessionContextAlloc(std.testing.allocator, &session, "fix bug", "inspect", .{ .max_bytes_preview = 8, .max_panel_summary = 80, .max_task = 16 });
|
||||
defer std.testing.allocator.free(text);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "context:v1\n") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "active_panel=diagnostics") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "panel_summary=") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "buffer_preview=pub") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "task=fix_bug") != null);
|
||||
}
|
||||
|
||||
test "regular: local context is line oriented and sanitized" {
|
||||
const text = try localContextAlloc(std.testing.allocator, "/tmp/mim repo", "ship v1", null, .{});
|
||||
defer std.testing.allocator.free(text);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "cwd=/tmp/mim_repo") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "task=ship_v1") != null);
|
||||
}
|
||||
|
||||
test "adversarial: context output is bounded" {
|
||||
const long = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
const text = try localContextAlloc(std.testing.allocator, long, long, long, .{ .max_panel_summary = 8, .max_task = 8 });
|
||||
defer std.testing.allocator.free(text);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "cwd=aaaaaaaa...") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "task=aaaaaaaa...") != null);
|
||||
}
|
||||
+76
-17
@@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const context_mod = @import("context.zig");
|
||||
const input = @import("input.zig");
|
||||
const job = @import("job.zig");
|
||||
const layout = @import("layout.zig");
|
||||
@@ -23,10 +24,12 @@ const help_text =
|
||||
\\Usage:
|
||||
\\ mim [--help]
|
||||
\\ mim [--version]
|
||||
\\ mim context [task] [intent]
|
||||
\\ mim mimctl context <socket|-> [task] [intent]
|
||||
\\ mim --mimctl <socket|-> <request...>
|
||||
\\
|
||||
\\This early build exposes the canonical smoke path and a minimal local
|
||||
\\session-control client for trusted tools.
|
||||
\\This early build exposes the canonical smoke path, compact local context,
|
||||
\\and a minimal local session-control client for trusted tools.
|
||||
\\
|
||||
;
|
||||
|
||||
@@ -63,6 +66,29 @@ pub fn main(init: std.process.Init) !u8 {
|
||||
|
||||
const first_arg = args.next();
|
||||
if (first_arg) |arg| {
|
||||
if (std.mem.eql(u8, arg, "context")) {
|
||||
try printLocalContext(allocator, init.io, &args, stdout);
|
||||
return 0;
|
||||
}
|
||||
if (std.mem.eql(u8, arg, "mimctl")) {
|
||||
const sub = args.next() orelse {
|
||||
try stderr.writeStreamingAll(init.io, "mim: mimctl requires a subcommand\n");
|
||||
return 64;
|
||||
};
|
||||
if (!std.mem.eql(u8, sub, "context")) {
|
||||
try stderr.writeStreamingAll(init.io, "mim: unknown mimctl subcommand\n");
|
||||
return 64;
|
||||
}
|
||||
const socket_path = args.next() orelse {
|
||||
try stderr.writeStreamingAll(init.io, "mim: mimctl context requires a socket path or '-'\n");
|
||||
return 64;
|
||||
};
|
||||
const line = try collectContextRequest(allocator, &args);
|
||||
defer allocator.free(line);
|
||||
requestMimctl(allocator, init.io, init.minimal.environ, stdout, stderr, socket_path, line) catch return 69;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, arg, "--mimctl")) {
|
||||
const socket_path = args.next() orelse {
|
||||
try stderr.writeStreamingAll(init.io, "mim: --mimctl requires a socket path\n");
|
||||
@@ -74,21 +100,7 @@ pub fn main(init: std.process.Init) !u8 {
|
||||
try stderr.writeStreamingAll(init.io, "mim: --mimctl requires a request\n");
|
||||
return 64;
|
||||
}
|
||||
const resolved_socket_path = if (std.mem.eql(u8, socket_path, "-"))
|
||||
std.process.Environ.getAlloc(init.minimal.environ, allocator, socket.socket_env_name) catch {
|
||||
try stderr.writeStreamingAll(init.io, "mim: MIM_SOCKET is not set\n");
|
||||
return 69;
|
||||
}
|
||||
else
|
||||
try allocator.dupe(u8, socket_path);
|
||||
defer allocator.free(resolved_socket_path);
|
||||
|
||||
const response = socket.request(allocator, resolved_socket_path, line) catch {
|
||||
try stderr.writeStreamingAll(init.io, "mim: mimctl request failed\n");
|
||||
return 69;
|
||||
};
|
||||
defer allocator.free(response);
|
||||
try stdout.writeStreamingAll(init.io, response);
|
||||
requestMimctl(allocator, init.io, init.minimal.environ, stdout, stderr, socket_path, line) catch return 69;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -113,6 +125,51 @@ pub fn main(init: std.process.Init) !u8 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn printLocalContext(allocator: std.mem.Allocator, io: std.Io, args: *std.process.Args.Iterator, stdout: std.Io.File) !void {
|
||||
const task = args.next();
|
||||
const intent = args.next();
|
||||
const text = try context_mod.localContextAlloc(allocator, ".", task, intent, .{});
|
||||
defer allocator.free(text);
|
||||
try stdout.writeStreamingAll(io, text);
|
||||
}
|
||||
|
||||
fn requestMimctl(
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
environ: std.process.Environ,
|
||||
stdout: std.Io.File,
|
||||
stderr: std.Io.File,
|
||||
socket_path: []const u8,
|
||||
line: []const u8,
|
||||
) !void {
|
||||
const resolved_socket_path = if (std.mem.eql(u8, socket_path, "-"))
|
||||
std.process.Environ.getAlloc(environ, allocator, socket.socket_env_name) catch {
|
||||
try stderr.writeStreamingAll(io, "mim: MIM_SOCKET is not set\n");
|
||||
return error.MimctlFailed;
|
||||
}
|
||||
else
|
||||
try allocator.dupe(u8, socket_path);
|
||||
defer allocator.free(resolved_socket_path);
|
||||
|
||||
const response = socket.request(allocator, resolved_socket_path, line) catch {
|
||||
try stderr.writeStreamingAll(io, "mim: mimctl request failed\n");
|
||||
return error.MimctlFailed;
|
||||
};
|
||||
defer allocator.free(response);
|
||||
try stdout.writeStreamingAll(io, response);
|
||||
}
|
||||
|
||||
fn collectContextRequest(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) ![]u8 {
|
||||
var line = std.ArrayList(u8).empty;
|
||||
errdefer line.deinit(allocator);
|
||||
try line.appendSlice(allocator, "context");
|
||||
while (args.next()) |arg| {
|
||||
try line.append(allocator, ' ');
|
||||
try line.appendSlice(allocator, arg);
|
||||
}
|
||||
return line.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn collectRemainingArgs(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) ![]u8 {
|
||||
var line = std.ArrayList(u8).empty;
|
||||
errdefer line.deinit(allocator);
|
||||
@@ -126,6 +183,7 @@ fn collectRemainingArgs(allocator: std.mem.Allocator, args: *std.process.Args.It
|
||||
}
|
||||
|
||||
test {
|
||||
_ = context_mod;
|
||||
_ = input;
|
||||
_ = job;
|
||||
_ = layout;
|
||||
@@ -146,6 +204,7 @@ test {
|
||||
test "regular: help text names the binary and smoke boundary" {
|
||||
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim - mobile-first terminal code editor") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, help_text, "canonical smoke path") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim context") != null);
|
||||
}
|
||||
|
||||
test "regular: version output is stable enough for smoke checks" {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const context_mod = @import("context.zig");
|
||||
const session_mod = @import("session.zig");
|
||||
|
||||
// Tiny local session protocol for trusted same-user tools.
|
||||
@@ -13,6 +14,8 @@ pub const max_request_bytes = 4096;
|
||||
pub fn handleLine(allocator: std.mem.Allocator, session: *session_mod.Session, line: []const u8) ![]u8 {
|
||||
const trimmed = std.mem.trimEnd(u8, line, "\r\n");
|
||||
if (std.mem.eql(u8, trimmed, "state")) return stateResponse(allocator, session);
|
||||
if (std.mem.eql(u8, trimmed, "context")) return context_mod.sessionContextAlloc(allocator, session, null, null, .{});
|
||||
if (std.mem.startsWith(u8, trimmed, "context ")) return contextResponse(allocator, session, trimmed[8..]);
|
||||
if (std.mem.startsWith(u8, trimmed, "open ")) {
|
||||
const bytes = trimmed[5..];
|
||||
if (!std.unicode.utf8ValidateSlice(bytes)) return allocator.dupe(u8, "err invalid utf8\n");
|
||||
@@ -25,6 +28,12 @@ pub fn handleLine(allocator: std.mem.Allocator, session: *session_mod.Session, l
|
||||
return allocator.dupe(u8, "err unknown request\n");
|
||||
}
|
||||
|
||||
fn contextResponse(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 {
|
||||
const split = std.mem.indexOfScalar(u8, payload, ' ');
|
||||
if (split) |index| return context_mod.sessionContextAlloc(allocator, session, payload[0..index], payload[index + 1 ..], .{});
|
||||
return context_mod.sessionContextAlloc(allocator, session, payload, null, .{});
|
||||
}
|
||||
|
||||
fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session, command: []const u8) ![]u8 {
|
||||
if (std.mem.eql(u8, command, "move_left")) return dispatchAndRespond(allocator, session, .move_left);
|
||||
if (std.mem.eql(u8, command, "move_right")) return dispatchAndRespond(allocator, session, .move_right);
|
||||
@@ -162,6 +171,21 @@ fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![
|
||||
);
|
||||
}
|
||||
|
||||
test "regular: protocol context request returns compact agent context" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
try session.openFixture("abc");
|
||||
try session.openListPanel("diag", &.{ "one", "two" });
|
||||
|
||||
const response = try handleLine(std.testing.allocator, &session, "context fix_bug inspect\n");
|
||||
defer std.testing.allocator.free(response);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "context:v1\n") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "active_panel=diag") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "panel_summary=") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "task=fix_bug") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "intent=inspect") != null);
|
||||
}
|
||||
|
||||
test "regular: protocol opens bytes, reports state, and dispatches commands" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
|
||||
@@ -244,3 +244,24 @@ test "regular: local socket panel command exposes inspectable panel state" {
|
||||
thread.join();
|
||||
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=empty\n", state);
|
||||
}
|
||||
|
||||
test "regular: local socket context request returns compact session context" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
try session.openFixture("abc");
|
||||
try session.openListPanel("diag", &.{ "one", "two" });
|
||||
|
||||
const path = "/tmp/mim-test-context.sock";
|
||||
var server = try Server.listen(std.testing.allocator, path, &session);
|
||||
defer server.deinit();
|
||||
|
||||
const thread = try std.Thread.spawn(.{}, Server.acceptOnce, .{&server});
|
||||
const response = try request(std.testing.allocator, path, "context fix_bug inspect");
|
||||
defer std.testing.allocator.free(response);
|
||||
thread.join();
|
||||
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "context:v1\n") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "active_panel=diag") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "task=fix_bug") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, response, "intent=inspect") != null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user