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
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