lua: implement data models with YAML serializer and parser
Swadhin Biswas committed 7 months ago
e68f19d
1 parent 8d47a77
commit e68f19d
1 file changed
+249 -0
Showing 1 changed file+249-0
▼LUA/contexa/models.lua
+249-0
| @@ -0,0 +1,249 @@ | ||
1 | +--- contexa — Git-inspired context management for LLM agents | |
2 | +--- COMMIT, BRANCH, MERGE, and CONTEXT operations over a persistent | |
3 | +--- versioned memory workspace. Based on arXiv:2508.00031. | |
4 | +--- | |
5 | +--- @module contexa.models | |
6 | + | |
7 | +local M = {} | |
8 | + | |
9 | +--- Generate an 8-character hex ID. | |
10 | +--- @return string | |
11 | +function M.generate_id() | |
12 | + local bytes = {} | |
13 | + for i = 1, 4 do | |
14 | + bytes[i] = string.format("%02x", math.random(0, 255)) | |
15 | + end | |
16 | + return table.concat(bytes) | |
17 | +end | |
18 | + | |
19 | +--- Get current UTC timestamp in ISO 8601 format. | |
20 | +--- @return string | |
21 | +function M.timestamp() | |
22 | + return os.date("!%Y-%m-%dT%H:%M:%S+00:00") | |
23 | +end | |
24 | + | |
25 | +------------------------------------------------------------------------ | |
26 | +-- OTARecord | |
27 | +------------------------------------------------------------------------ | |
28 | + | |
29 | +--- @class OTARecord | |
30 | +--- @field step number | |
31 | +--- @field timestamp string | |
32 | +--- @field observation string | |
33 | +--- @field thought string | |
34 | +--- @field action string | |
35 | + | |
36 | +--- Create a new OTARecord. | |
37 | +--- @param step number | |
38 | +--- @param observation string | |
39 | +--- @param thought string | |
40 | +--- @param action string | |
41 | +--- @param ts string|nil | |
42 | +--- @return OTARecord | |
43 | +function M.new_ota(step, observation, thought, action, ts) | |
44 | + return { | |
45 | + step = step, | |
46 | + timestamp = ts or M.timestamp(), | |
47 | + observation = observation or "", | |
48 | + thought = thought or "", | |
49 | + action = action or "", | |
50 | + } | |
51 | +end | |
52 | + | |
53 | +--- Render an OTARecord as markdown. | |
54 | +--- @param rec OTARecord | |
55 | +--- @return string | |
56 | +function M.ota_to_markdown(rec) | |
57 | + return string.format( | |
58 | + "### Step %d — %s\n**Observation:** %s\n\n**Thought:** %s\n\n**Action:** %s\n\n---\n", | |
59 | + rec.step, rec.timestamp, | |
60 | + rec.observation, rec.thought, rec.action | |
61 | + ) | |
62 | +end | |
63 | + | |
64 | +------------------------------------------------------------------------ | |
65 | +-- CommitRecord | |
66 | +------------------------------------------------------------------------ | |
67 | + | |
68 | +--- @class CommitRecord | |
69 | +--- @field commit_id string | |
70 | +--- @field branch_name string | |
71 | +--- @field branch_purpose string | |
72 | +--- @field previous_progress_summary string | |
73 | +--- @field this_commit_contribution string | |
74 | +--- @field timestamp string | |
75 | + | |
76 | +--- Create a new CommitRecord. | |
77 | +--- @param branch_name string | |
78 | +--- @param branch_purpose string | |
79 | +--- @param previous_progress_summary string | |
80 | +--- @param this_commit_contribution string | |
81 | +--- @param ts string|nil | |
82 | +--- @return CommitRecord | |
83 | +function M.new_commit(branch_name, branch_purpose, previous_progress_summary, this_commit_contribution, ts) | |
84 | + return { | |
85 | + commit_id = M.generate_id(), | |
86 | + branch_name = branch_name, | |
87 | + branch_purpose = branch_purpose or "", | |
88 | + previous_progress_summary = previous_progress_summary or "", | |
89 | + this_commit_contribution = this_commit_contribution or "", | |
90 | + timestamp = ts or M.timestamp(), | |
91 | + } | |
92 | +end | |
93 | + | |
94 | +--- Render a CommitRecord as markdown. | |
95 | +--- @param rec CommitRecord | |
96 | +--- @return string | |
97 | +function M.commit_to_markdown(rec) | |
98 | + return string.format( | |
99 | + "## Commit `%s`\n**Timestamp:** %s\n\n**Branch Purpose:** %s\n\n**Previous Progress Summary:** %s\n\n**This Commit's Contribution:** %s\n\n---\n", | |
100 | + rec.commit_id, rec.timestamp, | |
101 | + rec.branch_purpose, rec.previous_progress_summary, | |
102 | + rec.this_commit_contribution | |
103 | + ) | |
104 | +end | |
105 | + | |
106 | +------------------------------------------------------------------------ | |
107 | +-- BranchMetadata | |
108 | +------------------------------------------------------------------------ | |
109 | + | |
110 | +--- @class BranchMetadata | |
111 | +--- @field name string | |
112 | +--- @field purpose string | |
113 | +--- @field created_from string | |
114 | +--- @field created_at string | |
115 | +--- @field status string | |
116 | +--- @field merged_into string|nil | |
117 | +--- @field merged_at string|nil | |
118 | + | |
119 | +--- Create a new BranchMetadata. | |
120 | +--- @param name string | |
121 | +--- @param purpose string | |
122 | +--- @param created_from string | |
123 | +--- @param ts string|nil | |
124 | +--- @return BranchMetadata | |
125 | +function M.new_branch_metadata(name, purpose, created_from, ts) | |
126 | + return { | |
127 | + name = name, | |
128 | + purpose = purpose, | |
129 | + created_from = created_from or "", | |
130 | + created_at = ts or M.timestamp(), | |
131 | + status = "active", | |
132 | + merged_into = nil, | |
133 | + merged_at = nil, | |
134 | + } | |
135 | +end | |
136 | + | |
137 | +--- Serialize BranchMetadata to YAML string. | |
138 | +--- We write our own minimal serializer to avoid external YAML deps. | |
139 | +--- @param meta BranchMetadata | |
140 | +--- @return string | |
141 | +function M.metadata_to_yaml(meta) | |
142 | + local function yaml_val(v) | |
143 | + if v == nil then return "null" end | |
144 | + if v == "" then return "''" end | |
145 | + -- Quote strings that contain colons or special chars | |
146 | + if v:find(":") or v:find("#") or v:find("'") then | |
147 | + return "'" .. v:gsub("'", "''") .. "'" | |
148 | + end | |
149 | + return v | |
150 | + end | |
151 | + -- Write keys in sorted order for cross-language compatibility | |
152 | + local lines = { | |
153 | + "created_at: " .. yaml_val(meta.created_at), | |
154 | + "created_from: " .. yaml_val(meta.created_from), | |
155 | + "merged_at: " .. yaml_val(meta.merged_at), | |
156 | + "merged_into: " .. yaml_val(meta.merged_into), | |
157 | + "name: " .. yaml_val(meta.name), | |
158 | + "purpose: " .. yaml_val(meta.purpose), | |
159 | + "status: " .. yaml_val(meta.status), | |
160 | + } | |
161 | + return table.concat(lines, "\n") .. "\n" | |
162 | +end | |
163 | + | |
164 | +--- Parse BranchMetadata from YAML string. | |
165 | +--- @param text string | |
166 | +--- @return BranchMetadata | |
167 | +function M.metadata_from_yaml(text) | |
168 | + local meta = { | |
169 | + name = "", | |
170 | + purpose = "", | |
171 | + created_from = "", | |
172 | + created_at = "", | |
173 | + status = "active", | |
174 | + merged_into = nil, | |
175 | + merged_at = nil, | |
176 | + } | |
177 | + for line in text:gmatch("[^\n]+") do | |
178 | + local key, val = line:match("^(%S+):%s*(.*)$") | |
179 | + if key and val then | |
180 | + -- Strip surrounding quotes | |
181 | + val = val:match("^'(.*)'$") or val | |
182 | + if val == "null" then val = nil | |
183 | + elseif val == "''" then val = "" | |
184 | + end | |
185 | + if key == "name" then meta.name = val or "" | |
186 | + elseif key == "purpose" then meta.purpose = val or "" | |
187 | + elseif key == "created_from" then meta.created_from = val or "" | |
188 | + elseif key == "created_at" then meta.created_at = val or "" | |
189 | + elseif key == "status" then meta.status = val or "active" | |
190 | + elseif key == "merged_into" then meta.merged_into = val | |
191 | + elseif key == "merged_at" then meta.merged_at = val | |
192 | + end | |
193 | + end | |
194 | + end | |
195 | + return meta | |
196 | +end | |
197 | + | |
198 | +------------------------------------------------------------------------ | |
199 | +-- ContextResult | |
200 | +------------------------------------------------------------------------ | |
201 | + | |
202 | +--- @class ContextResult | |
203 | +--- @field branch_name string | |
204 | +--- @field k number | |
205 | +--- @field commits CommitRecord[] | |
206 | +--- @field ota_records OTARecord[] | |
207 | +--- @field main_roadmap string | |
208 | +--- @field metadata BranchMetadata|nil | |
209 | + | |
210 | +--- Create a new ContextResult. | |
211 | +--- @param branch_name string | |
212 | +--- @param k number | |
213 | +--- @return ContextResult | |
214 | +function M.new_context_result(branch_name, k) | |
215 | + return { | |
216 | + branch_name = branch_name, | |
217 | + k = k, | |
218 | + commits = {}, | |
219 | + ota_records = {}, | |
220 | + main_roadmap = "", | |
221 | + metadata = nil, | |
222 | + } | |
223 | +end | |
224 | + | |
225 | +--- Render ContextResult summary as markdown. | |
226 | +--- @param ctx ContextResult | |
227 | +--- @return string | |
228 | +function M.context_summary(ctx) | |
229 | + local parts = {} | |
230 | + table.insert(parts, string.format("# CONTEXT — branch `%s` (K=%d)\n", ctx.branch_name, ctx.k)) | |
231 | + | |
232 | + table.insert(parts, "## Global Roadmap\n" .. ctx.main_roadmap .. "\n") | |
233 | + | |
234 | + table.insert(parts, string.format("## Last %d Commit(s)\n", ctx.k)) | |
235 | + for _, c in ipairs(ctx.commits) do | |
236 | + table.insert(parts, M.commit_to_markdown(c)) | |
237 | + end | |
238 | + | |
239 | + local total = #ctx.ota_records | |
240 | + table.insert(parts, string.format("## Recent OTA steps (%d total)\n", total)) | |
241 | + local start = math.max(1, total - 4) | |
242 | + for i = start, total do | |
243 | + table.insert(parts, M.ota_to_markdown(ctx.ota_records[i])) | |
244 | + end | |
245 | + | |
246 | + return table.concat(parts, "\n") | |
247 | +end | |
248 | + | |
249 | +return M | |
0 | 250 | |