zig: implement full GCC workspace in src/main.zig

Swadhin Biswas committed 7 months ago
30573eb
1 parent 2028438
commit 30573eb
1 file changed
+577 -0
Showing 1 changed file+577-0
▼ZIG/src/main.zig
+577-0
1
+//! contexa — Git-inspired context management for LLM agents.
2
+//! COMMIT, BRANCH, MERGE, and CONTEXT over versioned memory.
3
+//!
4
+//! Paper: "Git Context Controller: Manage the Context of LLM-based Agents like Git"
5
+//! arXiv:2508.00031 — Junde Wu et al., 2025
6
+//!
7
+//! File system layout:
8
+//! .GCC/
9
+//! ├── main.md # Global roadmap / planning artifact
10
+//! └── branches/
11
+//! ├── main/
12
+//! │ ├── log.md # Continuous OTA trace (Observation-Thought-Action)
13
+//! │ ├── commit.md # Milestone-level commit summaries
14
+//! │ └── metadata.yaml # Branch intent, status, creation info
15
+//! └── <branch>/
16
+//! └── ...
17
+
18
+const std = @import("std");
19
+const fs = std.fs;
20
+const mem = std.mem;
21
+const fmt = std.fmt;
22
+
23
+pub const MAIN_BRANCH = "main";
24
+pub const GCC_DIR = ".GCC";
25
+
26
+// ------------------------------------------------------------------ //
27
+// Data Models //
28
+// ------------------------------------------------------------------ //
29
+
30
+/// A single Observation–Thought–Action cycle (logged to log.md).
31
+/// The paper logs continuous OTA cycles as the agent executes.
32
+pub const OTARecord = struct {
33
+ step: usize,
34
+ timestamp: []const u8,
35
+ observation: []const u8,
36
+ thought: []const u8,
37
+ action: []const u8,
38
+
39
+ /// Write this record to the log.md format used by the paper.
40
+ pub fn writeMarkdown(self: OTARecord, writer: anytype) !void {
41
+ try writer.print(
42
+ "### Step {d} — {s}\n**Observation:** {s}\n\n**Thought:** {s}\n\n**Action:** {s}\n\n---\n",
43
+ .{ self.step, self.timestamp, self.observation, self.thought, self.action },
44
+ );
45
+ }
46
+};
47
+
48
+/// A commit checkpoint (paper §3.2).
49
+/// Fields: Branch Purpose, Previous Progress Summary, This Commit's Contribution.
50
+pub const CommitRecord = struct {
51
+ commit_id: []const u8,
52
+ branch_name: []const u8,
53
+ branch_purpose: []const u8,
54
+ previous_progress_summary: []const u8,
55
+ this_commit_contribution: []const u8,
56
+ timestamp: []const u8,
57
+
58
+ pub fn writeMarkdown(self: CommitRecord, writer: anytype) !void {
59
+ try writer.print(
60
+ "## Commit `{s}`\n**Timestamp:** {s}\n\n**Branch Purpose:** {s}\n\n" ++
61
+ "**Previous Progress Summary:** {s}\n\n" ++
62
+ "**This Commit's Contribution:** {s}\n\n---\n",
63
+ .{
64
+ self.commit_id, self.timestamp,
65
+ self.branch_purpose, self.previous_progress_summary,
66
+ self.this_commit_contribution,
67
+ },
68
+ );
69
+ }
70
+};
71
+
72
+/// Branch metadata written to metadata.yaml (paper §3.1).
73
+pub const BranchMetadata = struct {
74
+ name: []const u8,
75
+ purpose: []const u8,
76
+ created_from: []const u8,
77
+ created_at: []const u8,
78
+ status: []const u8, // "active" | "merged" | "abandoned"
79
+ merged_into: ?[]const u8 = null,
80
+ merged_at: ?[]const u8 = null,
81
+
82
+ pub fn writeYaml(self: BranchMetadata, writer: anytype) !void {
83
+ try writer.print(
84
+ "name: {s}\npurpose: {s}\ncreated_from: {s}\ncreated_at: {s}\nstatus: {s}\n",
85
+ .{ self.name, self.purpose, self.created_from, self.created_at, self.status },
86
+ );
87
+ if (self.merged_into) |mi| {
88
+ try writer.print("merged_into: {s}\n", .{mi});
89
+ } else {
90
+ try writer.writeAll("merged_into: null\n");
91
+ }
92
+ if (self.merged_at) |ma| {
93
+ try writer.print("merged_at: {s}\n", .{ma});
94
+ } else {
95
+ try writer.writeAll("merged_at: null\n");
96
+ }
97
+ }
98
+};
99
+
100
+// ------------------------------------------------------------------ //
101
+// Workspace //
102
+// ------------------------------------------------------------------ //
103
+
104
+/// Manages the .GCC/ directory structure for one agent project.
105
+/// Implements the four GCC commands from arXiv:2508.00031v2.
106
+pub const Workspace = struct {
107
+ allocator: mem.Allocator,
108
+ root: []const u8,
109
+ current_branch: []const u8,
110
+
111
+ const Self = @This();
112
+
113
+ pub fn init(allocator: mem.Allocator, project_root: []const u8) Self {
114
+ return .{
115
+ .allocator = allocator,
116
+ .root = project_root,
117
+ .current_branch = MAIN_BRANCH,
118
+ };
119
+ }
120
+
121
+ // ---------------------------------------------------------------- //
122
+ // Path helpers //
123
+ // ---------------------------------------------------------------- //
124
+
125
+ fn gccPath(self: Self) ![]u8 {
126
+ return fs.path.join(self.allocator, &.{ self.root, GCC_DIR });
127
+ }
128
+
129
+ fn branchDir(self: Self, b: []const u8) ![]u8 {
130
+ return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b });
131
+ }
132
+
133
+ fn logPath(self: Self, b: []const u8) ![]u8 {
134
+ return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b, "log.md" });
135
+ }
136
+
137
+ fn commitFilePath(self: Self, b: []const u8) ![]u8 {
138
+ return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b, "commit.md" });
139
+ }
140
+
141
+ fn metaFilePath(self: Self, b: []const u8) ![]u8 {
142
+ return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b, "metadata.yaml" });
143
+ }
144
+
145
+ fn mainMdPath(self: Self) ![]u8 {
146
+ return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "main.md" });
147
+ }
148
+
149
+ // ---------------------------------------------------------------- //
150
+ // I/O helpers //
151
+ // ---------------------------------------------------------------- //
152
+
153
+ fn writeFile(self: Self, path: []const u8, content: []const u8) !void {
154
+ _ = self;
155
+ const file = try fs.createFileAbsolute(path, .{ .truncate = true });
156
+ defer file.close();
157
+ try file.writeAll(content);
158
+ }
159
+
160
+ fn appendToFile(self: Self, path: []const u8, content: []const u8) !void {
161
+ _ = self;
162
+ const file = try fs.openFileAbsolute(path, .{ .mode = .write_only });
163
+ defer file.close();
164
+ try file.seekFromEnd(0);
165
+ try file.writeAll(content);
166
+ }
167
+
168
+ fn readFile(self: Self, path: []const u8) ![]u8 {
169
+ const file = fs.openFileAbsolute(path, .{}) catch return try self.allocator.dupe(u8, "");
170
+ defer file.close();
171
+ return file.readToEndAlloc(self.allocator, 10 * 1024 * 1024);
172
+ }
173
+
174
+ fn countOTASteps(self: Self, b: []const u8) !usize {
175
+ const path = try self.logPath(b);
176
+ defer self.allocator.free(path);
177
+ const content = try self.readFile(path);
178
+ defer self.allocator.free(content);
179
+ var count: usize = 0;
180
+ var it = mem.splitSequence(u8, content, "### Step ");
181
+ _ = it.next(); // skip header
182
+ while (it.next()) |_| count += 1;
183
+ return count;
184
+ }
185
+
186
+ fn countCommits(self: Self, b: []const u8) !usize {
187
+ const path = try self.commitFilePath(b);
188
+ defer self.allocator.free(path);
189
+ const content = try self.readFile(path);
190
+ defer self.allocator.free(content);
191
+ var count: usize = 0;
192
+ var it = mem.splitSequence(u8, content, "## Commit `");
193
+ _ = it.next(); // skip header
194
+ while (it.next()) |_| count += 1;
195
+ return count;
196
+ }
197
+
198
+ fn nowTimestamp(_: Self) []const u8 {
199
+ // Returns a static placeholder; in production use std.time
200
+ return "2025-01-01T00:00:00Z";
201
+ }
202
+
203
+ fn generateID(self: Self) ![]u8 {
204
+ var rng = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp()));
205
+ const r = rng.random();
206
+ const id = try fmt.allocPrint(self.allocator, "{x:0>8}", .{r.int(u32)});
207
+ return id;
208
+ }
209
+
210
+ // ---------------------------------------------------------------- //
211
+ // GCC Workspace Initialisation //
212
+ // ---------------------------------------------------------------- //
213
+
214
+ /// Initialise a new GCC workspace.
215
+ /// Creates .GCC/ structure: main.md, branches/main/{log,commit,metadata}.
216
+ pub fn create(self: *Self, project_roadmap: []const u8) !void {
217
+ const gcc = try self.gccPath();
218
+ defer self.allocator.free(gcc);
219
+
220
+ // Create directory structure
221
+ const branch_dir = try self.branchDir(MAIN_BRANCH);
222
+ defer self.allocator.free(branch_dir);
223
+ try fs.makeDirAbsolute(gcc);
224
+ const branches_path = try fs.path.join(self.allocator, &.{ gcc, "branches" });
225
+ defer self.allocator.free(branches_path);
226
+ try fs.makeDirAbsolute(branches_path);
227
+ try fs.makeDirAbsolute(branch_dir);
228
+
229
+ // main.md — global roadmap
230
+ const main_path = try self.mainMdPath();
231
+ defer self.allocator.free(main_path);
232
+ const roadmap = try fmt.allocPrint(
233
+ self.allocator,
234
+ "# Project Roadmap\n\n**Initialized:** {s}\n\n{s}\n",
235
+ .{ self.nowTimestamp(), project_roadmap },
236
+ );
237
+ defer self.allocator.free(roadmap);
238
+ try self.writeFile(main_path, roadmap);
239
+
240
+ // log.md
241
+ const log = try self.logPath(MAIN_BRANCH);
242
+ defer self.allocator.free(log);
243
+ try self.writeFile(log, "# OTA Log — branch `main`\n\n");
244
+
245
+ // commit.md
246
+ const commit_file = try self.commitFilePath(MAIN_BRANCH);
247
+ defer self.allocator.free(commit_file);
248
+ try self.writeFile(commit_file, "# Commit History — branch `main`\n\n");
249
+
250
+ // metadata.yaml
251
+ const meta_file = try self.metaFilePath(MAIN_BRANCH);
252
+ defer self.allocator.free(meta_file);
253
+ var buf: std.ArrayListUnmanaged(u8) = .empty;
254
+ defer buf.deinit(self.allocator);
255
+ const meta = BranchMetadata{
256
+ .name = MAIN_BRANCH,
257
+ .purpose = "Primary reasoning trajectory",
258
+ .created_from = "",
259
+ .created_at = self.nowTimestamp(),
260
+ .status = "active",
261
+ };
262
+ try meta.writeYaml(buf.writer(self.allocator));
263
+ try self.writeFile(meta_file, buf.items);
264
+
265
+ self.current_branch = MAIN_BRANCH;
266
+ }
267
+
268
+ // ---------------------------------------------------------------- //
269
+ // GCC Commands //
270
+ // ---------------------------------------------------------------- //
271
+
272
+ /// Append an OTA step to current branch's log.md.
273
+ /// The paper logs continuous Observation–Thought–Action cycles.
274
+ pub fn logOTA(
275
+ self: *Self,
276
+ observation: []const u8,
277
+ thought: []const u8,
278
+ action: []const u8,
279
+ ) !OTARecord {
280
+ const step = (try self.countOTASteps(self.current_branch)) + 1;
281
+ const record = OTARecord{
282
+ .step = step,
283
+ .timestamp = self.nowTimestamp(),
284
+ .observation = observation,
285
+ .thought = thought,
286
+ .action = action,
287
+ };
288
+ const log = try self.logPath(self.current_branch);
289
+ defer self.allocator.free(log);
290
+
291
+ var buf: std.ArrayListUnmanaged(u8) = .empty;
292
+ defer buf.deinit(self.allocator);
293
+ try record.writeMarkdown(buf.writer(self.allocator));
294
+ try self.appendToFile(log, buf.items);
295
+ return record;
296
+ }
297
+
298
+ /// COMMIT command (paper §3.2).
299
+ /// Checkpoints milestone: Branch Purpose, Previous Progress Summary,
300
+ /// This Commit's Contribution.
301
+ pub fn commit(
302
+ self: *Self,
303
+ contribution: []const u8,
304
+ previous_summary: ?[]const u8,
305
+ ) !CommitRecord {
306
+ const id = try self.generateID();
307
+
308
+ const prev = previous_summary orelse "Initial state — no prior commits.";
309
+
310
+ const record = CommitRecord{
311
+ .commit_id = id,
312
+ .branch_name = self.current_branch,
313
+ .branch_purpose = "Active branch",
314
+ .previous_progress_summary = prev,
315
+ .this_commit_contribution = contribution,
316
+ .timestamp = self.nowTimestamp(),
317
+ };
318
+
319
+ const commit_file = try self.commitFilePath(self.current_branch);
320
+ defer self.allocator.free(commit_file);
321
+
322
+ var buf: std.ArrayListUnmanaged(u8) = .empty;
323
+ defer buf.deinit(self.allocator);
324
+ try record.writeMarkdown(buf.writer(self.allocator));
325
+ try self.appendToFile(commit_file, buf.items);
326
+
327
+ // Return record (commit_id is already owned by caller via generateID)
328
+ return CommitRecord{
329
+ .commit_id = id,
330
+ .branch_name = self.current_branch,
331
+ .branch_purpose = "Active branch",
332
+ .previous_progress_summary = prev,
333
+ .this_commit_contribution = contribution,
334
+ .timestamp = self.nowTimestamp(),
335
+ };
336
+ }
337
+
338
+ /// BRANCH command (paper §3.3).
339
+ /// Creates isolated workspace: B_t^(name) = BRANCH(M_{t-1}).
340
+ pub fn branch(self: *Self, name: []const u8, purpose: []const u8) !void {
341
+ const branch_dir = try self.branchDir(name);
342
+ defer self.allocator.free(branch_dir);
343
+ try fs.makeDirAbsolute(branch_dir);
344
+
345
+ // Empty OTA log (fresh execution trace, per paper §3.3)
346
+ const log = try self.logPath(name);
347
+ defer self.allocator.free(log);
348
+ const log_header = try fmt.allocPrint(
349
+ self.allocator,
350
+ "# OTA Log — branch `{s}`\n\n",
351
+ .{name},
352
+ );
353
+ defer self.allocator.free(log_header);
354
+ try self.writeFile(log, log_header);
355
+
356
+ // Empty commit.md
357
+ const commit_file = try self.commitFilePath(name);
358
+ defer self.allocator.free(commit_file);
359
+ const commit_header = try fmt.allocPrint(
360
+ self.allocator,
361
+ "# Commit History — branch `{s}`\n\n",
362
+ .{name},
363
+ );
364
+ defer self.allocator.free(commit_header);
365
+ try self.writeFile(commit_file, commit_header);
366
+
367
+ // metadata.yaml records intent and motivation (paper §3.3)
368
+ const meta_file = try self.metaFilePath(name);
369
+ defer self.allocator.free(meta_file);
370
+ var buf: std.ArrayListUnmanaged(u8) = .empty;
371
+ defer buf.deinit(self.allocator);
372
+ const meta = BranchMetadata{
373
+ .name = name,
374
+ .purpose = purpose,
375
+ .created_from = self.current_branch,
376
+ .created_at = self.nowTimestamp(),
377
+ .status = "active",
378
+ };
379
+ try meta.writeYaml(buf.writer(self.allocator));
380
+ try self.writeFile(meta_file, buf.items);
381
+
382
+ self.current_branch = name;
383
+ }
384
+
385
+ /// MERGE command (paper §3.4).
386
+ /// Integrates branch into target, merging summaries and OTA traces.
387
+ pub fn merge(self: *Self, branch_name: []const u8, target: []const u8) !CommitRecord {
388
+ // Append branch OTA to target's log
389
+ const src_log = try self.logPath(branch_name);
390
+ defer self.allocator.free(src_log);
391
+ const src_content = try self.readFile(src_log);
392
+ defer self.allocator.free(src_content);
393
+
394
+ if (src_content.len > 0) {
395
+ const target_log = try self.logPath(target);
396
+ defer self.allocator.free(target_log);
397
+ const header = try fmt.allocPrint(
398
+ self.allocator,
399
+ "\n## Merged from `{s}` ({s})\n\n",
400
+ .{ branch_name, self.nowTimestamp() },
401
+ );
402
+ defer self.allocator.free(header);
403
+ try self.appendToFile(target_log, header);
404
+ try self.appendToFile(target_log, src_content);
405
+ }
406
+
407
+ // Switch to target and commit the merge
408
+ self.current_branch = target;
409
+ const summary = try fmt.allocPrint(
410
+ self.allocator,
411
+ "Merged branch `{s}` into `{s}`",
412
+ .{ branch_name, target },
413
+ );
414
+ // Note: summary is owned by allocator; commit will reference it, then we free
415
+ const merge_commit = try self.commit(summary, null);
416
+
417
+ // Update branch metadata to mark as merged
418
+ const meta_file = try self.metaFilePath(branch_name);
419
+ defer self.allocator.free(meta_file);
420
+ var buf: std.ArrayListUnmanaged(u8) = .empty;
421
+ defer buf.deinit(self.allocator);
422
+ const meta = BranchMetadata{
423
+ .name = branch_name,
424
+ .purpose = "Merged branch",
425
+ .created_from = "",
426
+ .created_at = self.nowTimestamp(),
427
+ .status = "merged",
428
+ .merged_into = target,
429
+ .merged_at = self.nowTimestamp(),
430
+ };
431
+ try meta.writeYaml(buf.writer(self.allocator));
432
+ try self.writeFile(meta_file, buf.items);
433
+
434
+ return merge_commit;
435
+ }
436
+
437
+ /// CONTEXT command (paper §3.5).
438
+ /// Reads the K most-recent commits + OTA log from the given branch.
439
+ /// Paper experiments fix K=1 (most recent commit record only).
440
+ pub fn context(self: Self, branch_name: ?[]const u8, k: usize) !ContextSnapshot {
441
+ const target = branch_name orelse self.current_branch;
442
+
443
+ const main_path = try self.mainMdPath();
444
+ defer self.allocator.free(main_path);
445
+ const roadmap = try self.readFile(main_path);
446
+
447
+ const log = try self.logPath(target);
448
+ defer self.allocator.free(log);
449
+ const ota_content = try self.readFile(log);
450
+
451
+ const commit_file = try self.commitFilePath(target);
452
+ defer self.allocator.free(commit_file);
453
+ const commit_content = try self.readFile(commit_file);
454
+
455
+ return ContextSnapshot{
456
+ .branch_name = target,
457
+ .k = k,
458
+ .main_roadmap = roadmap,
459
+ .ota_log = ota_content,
460
+ .commit_history = commit_content,
461
+ };
462
+ }
463
+
464
+ pub fn currentBranch(self: Self) []const u8 {
465
+ return self.current_branch;
466
+ }
467
+
468
+ pub fn switchBranch(self: *Self, name: []const u8) void {
469
+ self.current_branch = name;
470
+ }
471
+};
472
+
473
+/// Snapshot returned by the CONTEXT command (paper §3.5).
474
+/// The caller is responsible for freeing main_roadmap, ota_log, and commit_history.
475
+pub const ContextSnapshot = struct {
476
+ branch_name: []const u8,
477
+ k: usize,
478
+ main_roadmap: []const u8,
479
+ ota_log: []const u8,
480
+ commit_history: []const u8,
481
+
482
+ /// Free all owned memory.
483
+ pub fn deinit(self: *const ContextSnapshot, allocator: mem.Allocator) void {
484
+ allocator.free(self.main_roadmap);
485
+ allocator.free(self.ota_log);
486
+ allocator.free(self.commit_history);
487
+ }
488
+};
489
+
490
+// ------------------------------------------------------------------ //
491
+// Tests //
492
+// ------------------------------------------------------------------ //
493
+
494
+test "workspace init creates GCC structure" {
495
+ var tmp = std.testing.tmpDir(.{});
496
+ defer tmp.cleanup();
497
+
498
+ const root = try tmp.dir.realpathAlloc(std.testing.allocator, ".");
499
+ defer std.testing.allocator.free(root);
500
+
501
+ var ws = Workspace.init(std.testing.allocator, root);
502
+ try ws.create("Test project");
503
+
504
+ // Verify .GCC/main.md exists
505
+ const main_path = try ws.mainMdPath();
506
+ defer std.testing.allocator.free(main_path);
507
+ const f = try fs.openFileAbsolute(main_path, .{});
508
+ f.close();
509
+}
510
+
511
+test "logOTA increments step" {
512
+ var tmp = std.testing.tmpDir(.{});
513
+ defer tmp.cleanup();
514
+
515
+ const root = try tmp.dir.realpathAlloc(std.testing.allocator, ".");
516
+ defer std.testing.allocator.free(root);
517
+
518
+ var ws = Workspace.init(std.testing.allocator, root);
519
+ try ws.create("Test");
520
+
521
+ const r1 = try ws.logOTA("obs1", "thought1", "action1");
522
+ const r2 = try ws.logOTA("obs2", "thought2", "action2");
523
+ try std.testing.expectEqual(@as(usize, 1), r1.step);
524
+ try std.testing.expectEqual(@as(usize, 2), r2.step);
525
+}
526
+
527
+test "commit writes checkpoint" {
528
+ var tmp = std.testing.tmpDir(.{});
529
+ defer tmp.cleanup();
530
+
531
+ const root = try tmp.dir.realpathAlloc(std.testing.allocator, ".");
532
+ defer std.testing.allocator.free(root);
533
+
534
+ var ws = Workspace.init(std.testing.allocator, root);
535
+ try ws.create("Test");
536
+
537
+ const c = try ws.commit("Initial scaffold done", null);
538
+ defer std.testing.allocator.free(c.commit_id);
539
+ try std.testing.expectEqualStrings("Initial scaffold done", c.this_commit_contribution);
540
+ try std.testing.expectEqualStrings(MAIN_BRANCH, c.branch_name);
541
+}
542
+
543
+test "branch creates isolated workspace" {
544
+ var tmp = std.testing.tmpDir(.{});
545
+ defer tmp.cleanup();
546
+
547
+ const root = try tmp.dir.realpathAlloc(std.testing.allocator, ".");
548
+ defer std.testing.allocator.free(root);
549
+
550
+ var ws = Workspace.init(std.testing.allocator, root);
551
+ try ws.create("Test");
552
+ try ws.branch("experiment", "Try alternative");
553
+ try std.testing.expectEqualStrings("experiment", ws.currentBranch());
554
+
555
+ // Branch should have fresh empty OTA log
556
+ const step_count = try ws.countOTASteps("experiment");
557
+ try std.testing.expectEqual(@as(usize, 0), step_count);
558
+}
559
+
560
+test "context returns roadmap and history" {
561
+ var tmp = std.testing.tmpDir(.{});
562
+ defer tmp.cleanup();
563
+
564
+ const root = try tmp.dir.realpathAlloc(std.testing.allocator, ".");
565
+ defer std.testing.allocator.free(root);
566
+
567
+ var ws = Workspace.init(std.testing.allocator, root);
568
+ try ws.create("My AI project roadmap");
569
+ const c = try ws.commit("First milestone", null);
570
+ defer std.testing.allocator.free(c.commit_id);
571
+
572
+ const ctx = try ws.context(null, 1);
573
+ defer ctx.deinit(std.testing.allocator);
574
+
575
+ try std.testing.expect(mem.indexOf(u8, ctx.main_roadmap, "My AI project roadmap") != null);
576
+ try std.testing.expect(mem.indexOf(u8, ctx.commit_history, "First milestone") != null);
577
+}
0
578