abstractcode 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abstractcode/__init__.py +6 -37
- abstractcode/cli.py +401 -0
- abstractcode/flow_cli.py +1413 -0
- abstractcode/fullscreen_ui.py +1453 -0
- abstractcode/input_handler.py +81 -0
- abstractcode/py.typed +1 -0
- abstractcode/react_shell.py +6440 -0
- abstractcode/recall.py +384 -0
- abstractcode/remember.py +184 -0
- abstractcode/terminal_markdown.py +168 -0
- abstractcode/workflow_agent.py +894 -0
- abstractcode-0.3.0.dist-info/METADATA +270 -0
- abstractcode-0.3.0.dist-info/RECORD +17 -0
- abstractcode-0.1.0.dist-info/METADATA +0 -114
- abstractcode-0.1.0.dist-info/RECORD +0 -7
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/WHEEL +0 -0
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/entry_points.txt +0 -0
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/top_level.txt +0 -0
abstractcode/recall.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""AbstractCode recall helpers (no LLM required).
|
|
2
|
+
|
|
3
|
+
AbstractCode is a host UX; recall should stay consistent with runtime-owned
|
|
4
|
+
contracts. This module provides:
|
|
5
|
+
- lightweight argument parsing for `/recall`
|
|
6
|
+
- a thin execution helper that uses AbstractRuntime's ActiveContextPolicy
|
|
7
|
+
|
|
8
|
+
The goal is testability without requiring an LLM provider to be reachable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from abstractruntime.memory import ActiveContextPolicy, TimeRange
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class RecallRequest:
|
|
21
|
+
since: Optional[str] = None
|
|
22
|
+
until: Optional[str] = None
|
|
23
|
+
tags: Dict[str, Any] = None # type: ignore[assignment]
|
|
24
|
+
tags_mode: str = "all" # all|any
|
|
25
|
+
users: List[str] = None # type: ignore[assignment]
|
|
26
|
+
locations: List[str] = None # type: ignore[assignment]
|
|
27
|
+
query: Optional[str] = None
|
|
28
|
+
limit: int = 10
|
|
29
|
+
into_context: bool = False
|
|
30
|
+
placement: str = "after_summary"
|
|
31
|
+
show: bool = False
|
|
32
|
+
scope: str = "run" # run|session|global|all
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
object.__setattr__(self, "tags", dict(self.tags or {}))
|
|
36
|
+
|
|
37
|
+
raw_mode = str(getattr(self, "tags_mode", "") or "").strip().lower() or "all"
|
|
38
|
+
if raw_mode in ("and",):
|
|
39
|
+
raw_mode = "all"
|
|
40
|
+
if raw_mode in ("or",):
|
|
41
|
+
raw_mode = "any"
|
|
42
|
+
if raw_mode not in ("all", "any"):
|
|
43
|
+
raw_mode = "all"
|
|
44
|
+
object.__setattr__(self, "tags_mode", raw_mode)
|
|
45
|
+
|
|
46
|
+
def _norm_list(values: Any) -> List[str]:
|
|
47
|
+
if not isinstance(values, list):
|
|
48
|
+
return []
|
|
49
|
+
out: List[str] = []
|
|
50
|
+
for v in values:
|
|
51
|
+
if isinstance(v, str) and v.strip():
|
|
52
|
+
out.append(v.strip())
|
|
53
|
+
# preserve order but dedup (case-insensitive)
|
|
54
|
+
seen: set[str] = set()
|
|
55
|
+
deduped: List[str] = []
|
|
56
|
+
for s in out:
|
|
57
|
+
key = s.lower()
|
|
58
|
+
if key in seen:
|
|
59
|
+
continue
|
|
60
|
+
seen.add(key)
|
|
61
|
+
deduped.append(s)
|
|
62
|
+
return deduped
|
|
63
|
+
|
|
64
|
+
object.__setattr__(self, "users", _norm_list(getattr(self, "users", None)))
|
|
65
|
+
object.__setattr__(self, "locations", _norm_list(getattr(self, "locations", None)))
|
|
66
|
+
|
|
67
|
+
raw_scope = str(getattr(self, "scope", "") or "").strip().lower() or "run"
|
|
68
|
+
if raw_scope not in ("run", "session", "global", "all"):
|
|
69
|
+
raw_scope = "run"
|
|
70
|
+
object.__setattr__(self, "scope", raw_scope)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_recall_args(raw: str) -> RecallRequest:
|
|
74
|
+
"""Parse `/recall` arguments.
|
|
75
|
+
|
|
76
|
+
Supported flags:
|
|
77
|
+
- `--since ISO`
|
|
78
|
+
- `--until ISO`
|
|
79
|
+
- `--tag k=v` (repeatable)
|
|
80
|
+
- `--tags-mode all|any` (default all; AND/OR across tag keys)
|
|
81
|
+
- `--user NAME` (repeatable; alias: --users)
|
|
82
|
+
- `--location LOC` (repeatable; alias: --locations)
|
|
83
|
+
- `--q text` (if absent, remaining args become query)
|
|
84
|
+
- `--limit N`
|
|
85
|
+
- `--into-context`
|
|
86
|
+
- `--placement after_summary|after_system|end`
|
|
87
|
+
- `--show` (show full note content for memory_note matches)
|
|
88
|
+
- `--scope run|session|global|all`
|
|
89
|
+
"""
|
|
90
|
+
import shlex
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
parts = shlex.split(raw) if raw else []
|
|
94
|
+
except ValueError:
|
|
95
|
+
parts = raw.split() if raw else []
|
|
96
|
+
|
|
97
|
+
since: Optional[str] = None
|
|
98
|
+
until: Optional[str] = None
|
|
99
|
+
tags: Dict[str, Any] = {}
|
|
100
|
+
tags_mode = "all"
|
|
101
|
+
users: List[str] = []
|
|
102
|
+
locations: List[str] = []
|
|
103
|
+
query: Optional[str] = None
|
|
104
|
+
limit = 10
|
|
105
|
+
into_context = False
|
|
106
|
+
placement = "after_summary"
|
|
107
|
+
show = False
|
|
108
|
+
scope = "run"
|
|
109
|
+
|
|
110
|
+
leftovers: list[str] = []
|
|
111
|
+
i = 0
|
|
112
|
+
while i < len(parts):
|
|
113
|
+
p = parts[i]
|
|
114
|
+
if p in ("--since", "--from"):
|
|
115
|
+
if i + 1 >= len(parts):
|
|
116
|
+
raise ValueError("--since requires an ISO timestamp")
|
|
117
|
+
since = str(parts[i + 1])
|
|
118
|
+
i += 2
|
|
119
|
+
continue
|
|
120
|
+
if p in ("--until", "--to"):
|
|
121
|
+
if i + 1 >= len(parts):
|
|
122
|
+
raise ValueError("--until requires an ISO timestamp")
|
|
123
|
+
until = str(parts[i + 1])
|
|
124
|
+
i += 2
|
|
125
|
+
continue
|
|
126
|
+
if p in ("--tag", "--tags"):
|
|
127
|
+
if i + 1 >= len(parts):
|
|
128
|
+
raise ValueError("--tag requires k=v")
|
|
129
|
+
kv = str(parts[i + 1])
|
|
130
|
+
if "=" not in kv:
|
|
131
|
+
raise ValueError("--tag requires k=v")
|
|
132
|
+
k, v = kv.split("=", 1)
|
|
133
|
+
k = k.strip()
|
|
134
|
+
v = v.strip()
|
|
135
|
+
if not k or not v:
|
|
136
|
+
raise ValueError("--tag requires non-empty k=v")
|
|
137
|
+
if k != "kind":
|
|
138
|
+
prev = tags.get(k)
|
|
139
|
+
if prev is None:
|
|
140
|
+
tags[k] = v
|
|
141
|
+
elif isinstance(prev, str):
|
|
142
|
+
if prev != v:
|
|
143
|
+
tags[k] = [prev, v]
|
|
144
|
+
elif isinstance(prev, list):
|
|
145
|
+
if v not in prev:
|
|
146
|
+
prev.append(v)
|
|
147
|
+
i += 2
|
|
148
|
+
continue
|
|
149
|
+
if p in ("--tags-mode", "--tag-mode", "--tags-op", "--tag-op"):
|
|
150
|
+
if i + 1 >= len(parts):
|
|
151
|
+
raise ValueError("--tags-mode requires all|any")
|
|
152
|
+
mode = str(parts[i + 1]).strip().lower()
|
|
153
|
+
if mode in ("and",):
|
|
154
|
+
mode = "all"
|
|
155
|
+
if mode in ("or",):
|
|
156
|
+
mode = "any"
|
|
157
|
+
if mode not in ("all", "any"):
|
|
158
|
+
raise ValueError("--tags-mode must be all|any")
|
|
159
|
+
tags_mode = mode
|
|
160
|
+
i += 2
|
|
161
|
+
continue
|
|
162
|
+
if p in ("--user", "--users"):
|
|
163
|
+
if i + 1 >= len(parts):
|
|
164
|
+
raise ValueError("--user requires a name")
|
|
165
|
+
raw_user = str(parts[i + 1]).strip()
|
|
166
|
+
if raw_user:
|
|
167
|
+
for seg in raw_user.split(","):
|
|
168
|
+
s = seg.strip()
|
|
169
|
+
if s:
|
|
170
|
+
users.append(s)
|
|
171
|
+
i += 2
|
|
172
|
+
continue
|
|
173
|
+
if p in ("--location", "--locations"):
|
|
174
|
+
if i + 1 >= len(parts):
|
|
175
|
+
raise ValueError("--location requires a value")
|
|
176
|
+
raw_loc = str(parts[i + 1]).strip()
|
|
177
|
+
if raw_loc:
|
|
178
|
+
for seg in raw_loc.split(","):
|
|
179
|
+
s = seg.strip()
|
|
180
|
+
if s:
|
|
181
|
+
locations.append(s)
|
|
182
|
+
i += 2
|
|
183
|
+
continue
|
|
184
|
+
if p in ("--q", "--query"):
|
|
185
|
+
if i + 1 >= len(parts):
|
|
186
|
+
raise ValueError("--q requires a query string")
|
|
187
|
+
|
|
188
|
+
# Consume tokens until the next flag, so `--q player dies` works
|
|
189
|
+
# without requiring quotes.
|
|
190
|
+
j = i + 1
|
|
191
|
+
buf: list[str] = []
|
|
192
|
+
while j < len(parts) and not str(parts[j]).startswith("--"):
|
|
193
|
+
buf.append(str(parts[j]))
|
|
194
|
+
j += 1
|
|
195
|
+
query = " ".join([x for x in buf if x]).strip() or None
|
|
196
|
+
i = j
|
|
197
|
+
continue
|
|
198
|
+
if p == "--limit":
|
|
199
|
+
if i + 1 >= len(parts):
|
|
200
|
+
raise ValueError("--limit requires a number")
|
|
201
|
+
try:
|
|
202
|
+
limit = int(parts[i + 1])
|
|
203
|
+
except Exception:
|
|
204
|
+
raise ValueError("--limit requires a number") from None
|
|
205
|
+
if limit < 1:
|
|
206
|
+
limit = 1
|
|
207
|
+
i += 2
|
|
208
|
+
continue
|
|
209
|
+
if p == "--into-context":
|
|
210
|
+
into_context = True
|
|
211
|
+
i += 1
|
|
212
|
+
continue
|
|
213
|
+
if p == "--show":
|
|
214
|
+
show = True
|
|
215
|
+
i += 1
|
|
216
|
+
continue
|
|
217
|
+
if p == "--scope":
|
|
218
|
+
if i + 1 >= len(parts):
|
|
219
|
+
raise ValueError("--scope requires a value")
|
|
220
|
+
scope = str(parts[i + 1]).strip().lower() or "run"
|
|
221
|
+
if scope not in ("run", "session", "global", "all"):
|
|
222
|
+
raise ValueError("--scope must be run|session|global|all")
|
|
223
|
+
i += 2
|
|
224
|
+
continue
|
|
225
|
+
if p == "--placement":
|
|
226
|
+
if i + 1 >= len(parts):
|
|
227
|
+
raise ValueError("--placement requires a value")
|
|
228
|
+
placement = str(parts[i + 1]).strip()
|
|
229
|
+
if placement not in ("after_summary", "after_system", "end"):
|
|
230
|
+
raise ValueError("--placement must be after_summary|after_system|end")
|
|
231
|
+
i += 2
|
|
232
|
+
continue
|
|
233
|
+
if p.startswith("--"):
|
|
234
|
+
raise ValueError(f"Unknown flag: {p}")
|
|
235
|
+
|
|
236
|
+
leftovers.append(str(p))
|
|
237
|
+
i += 1
|
|
238
|
+
|
|
239
|
+
if query is None and leftovers:
|
|
240
|
+
query = " ".join([p for p in leftovers if p]).strip() or None
|
|
241
|
+
|
|
242
|
+
def _validate_iso(value: Optional[str], *, flag: str) -> None:
|
|
243
|
+
if value is None:
|
|
244
|
+
return
|
|
245
|
+
import datetime as _dt
|
|
246
|
+
|
|
247
|
+
v = str(value).strip()
|
|
248
|
+
if not v:
|
|
249
|
+
return
|
|
250
|
+
if v.endswith("Z"):
|
|
251
|
+
v = v[:-1] + "+00:00"
|
|
252
|
+
try:
|
|
253
|
+
_dt.datetime.fromisoformat(v)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
raise ValueError(f"{flag} must be ISO8601 (got: {value})") from e
|
|
256
|
+
|
|
257
|
+
_validate_iso(since, flag="--since")
|
|
258
|
+
_validate_iso(until, flag="--until")
|
|
259
|
+
|
|
260
|
+
return RecallRequest(
|
|
261
|
+
since=since,
|
|
262
|
+
until=until,
|
|
263
|
+
tags=tags,
|
|
264
|
+
tags_mode=tags_mode,
|
|
265
|
+
users=users,
|
|
266
|
+
locations=locations,
|
|
267
|
+
query=query,
|
|
268
|
+
limit=limit,
|
|
269
|
+
into_context=into_context,
|
|
270
|
+
placement=placement,
|
|
271
|
+
show=show,
|
|
272
|
+
scope=scope,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def execute_recall(
|
|
277
|
+
*,
|
|
278
|
+
run_id: str,
|
|
279
|
+
run_store: Any,
|
|
280
|
+
artifact_store: Any,
|
|
281
|
+
request: RecallRequest,
|
|
282
|
+
) -> Dict[str, Any]:
|
|
283
|
+
"""Execute a recall request against a run.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
dict with keys:
|
|
287
|
+
- matches: list[dict]
|
|
288
|
+
- rehydration: dict | None
|
|
289
|
+
"""
|
|
290
|
+
policy = ActiveContextPolicy(run_store=run_store, artifact_store=artifact_store)
|
|
291
|
+
|
|
292
|
+
def _resolve_session_root_id(start_run_id: str) -> str:
|
|
293
|
+
cur_id = str(start_run_id or "").strip()
|
|
294
|
+
seen: set[str] = set()
|
|
295
|
+
while cur_id and cur_id not in seen:
|
|
296
|
+
seen.add(cur_id)
|
|
297
|
+
st = run_store.load(cur_id)
|
|
298
|
+
if st is None:
|
|
299
|
+
return cur_id
|
|
300
|
+
parent = getattr(st, "parent_run_id", None)
|
|
301
|
+
if not isinstance(parent, str) or not parent.strip():
|
|
302
|
+
return cur_id
|
|
303
|
+
cur_id = parent.strip()
|
|
304
|
+
return str(start_run_id or "").strip()
|
|
305
|
+
|
|
306
|
+
def _global_memory_run_id() -> str:
|
|
307
|
+
import os
|
|
308
|
+
import re
|
|
309
|
+
|
|
310
|
+
rid = str(os.environ.get("ABSTRACTRUNTIME_GLOBAL_MEMORY_RUN_ID") or "").strip()
|
|
311
|
+
if rid and re.match(r"^[a-zA-Z0-9_-]+$", rid):
|
|
312
|
+
return rid
|
|
313
|
+
return "global_memory"
|
|
314
|
+
|
|
315
|
+
time_range: Optional[TimeRange] = None
|
|
316
|
+
if request.since or request.until:
|
|
317
|
+
time_range = TimeRange(start=request.since, end=request.until)
|
|
318
|
+
|
|
319
|
+
scope = str(request.scope or "run").strip().lower() or "run"
|
|
320
|
+
run_ids: list[str] = []
|
|
321
|
+
if scope == "run":
|
|
322
|
+
run_ids = [run_id]
|
|
323
|
+
elif scope == "session":
|
|
324
|
+
run_ids = [_resolve_session_root_id(run_id)]
|
|
325
|
+
elif scope == "global":
|
|
326
|
+
run_ids = [_global_memory_run_id()]
|
|
327
|
+
else: # all
|
|
328
|
+
root_id = _resolve_session_root_id(run_id)
|
|
329
|
+
global_id = _global_memory_run_id()
|
|
330
|
+
# Deterministic order; dedup.
|
|
331
|
+
seen_ids: set[str] = set()
|
|
332
|
+
for rid in (run_id, root_id, global_id):
|
|
333
|
+
if rid and rid not in seen_ids:
|
|
334
|
+
seen_ids.add(rid)
|
|
335
|
+
run_ids.append(rid)
|
|
336
|
+
|
|
337
|
+
matches: list[dict[str, Any]] = []
|
|
338
|
+
seen_artifacts: set[str] = set()
|
|
339
|
+
for rid in run_ids:
|
|
340
|
+
# Skip missing runs (e.g. global memory not created yet).
|
|
341
|
+
st = run_store.load(rid)
|
|
342
|
+
if st is None:
|
|
343
|
+
continue
|
|
344
|
+
part = policy.filter_spans(
|
|
345
|
+
rid,
|
|
346
|
+
time_range=time_range,
|
|
347
|
+
tags=(request.tags or None),
|
|
348
|
+
tags_mode=request.tags_mode,
|
|
349
|
+
authors=(request.users or None),
|
|
350
|
+
locations=(request.locations or None),
|
|
351
|
+
query=request.query,
|
|
352
|
+
limit=int(request.limit),
|
|
353
|
+
)
|
|
354
|
+
for s in part:
|
|
355
|
+
if not isinstance(s, dict):
|
|
356
|
+
continue
|
|
357
|
+
aid = str(s.get("artifact_id") or "").strip()
|
|
358
|
+
if not aid or aid in seen_artifacts:
|
|
359
|
+
continue
|
|
360
|
+
seen_artifacts.add(aid)
|
|
361
|
+
annotated = dict(s)
|
|
362
|
+
annotated["owner_run_id"] = rid
|
|
363
|
+
matches.append(annotated)
|
|
364
|
+
|
|
365
|
+
rehydration: Optional[Dict[str, Any]] = None
|
|
366
|
+
if request.into_context:
|
|
367
|
+
# Rehydrate the selected span(s) into active context. This is deterministic and persists
|
|
368
|
+
# the updated run state. Notes are rehydrated as a synthetic message.
|
|
369
|
+
span_ids: list[str] = []
|
|
370
|
+
for s in matches:
|
|
371
|
+
if not isinstance(s, dict):
|
|
372
|
+
continue
|
|
373
|
+
aid = s.get("artifact_id")
|
|
374
|
+
if isinstance(aid, str) and aid:
|
|
375
|
+
span_ids.append(aid)
|
|
376
|
+
if span_ids:
|
|
377
|
+
rehydration = policy.rehydrate_into_context(
|
|
378
|
+
run_id,
|
|
379
|
+
span_ids=span_ids,
|
|
380
|
+
placement=request.placement,
|
|
381
|
+
dedup_by="message_id",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return {"matches": matches, "rehydration": rehydration}
|
abstractcode/remember.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""AbstractCode remember helpers (no LLM required).
|
|
2
|
+
|
|
3
|
+
AbstractCode is a host UX; "remember" should be implemented via runtime-owned
|
|
4
|
+
memory primitives so behavior stays consistent across hosts.
|
|
5
|
+
|
|
6
|
+
This module provides:
|
|
7
|
+
- lightweight argument parsing for `/memorize`
|
|
8
|
+
- an execution helper that stores a runtime `MEMORY_NOTE` targeting a run
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class RememberRequest:
|
|
19
|
+
note: str
|
|
20
|
+
tags: Dict[str, str]
|
|
21
|
+
span_id: Optional[str] = None
|
|
22
|
+
last_span: bool = False
|
|
23
|
+
last_messages: int = 6
|
|
24
|
+
scope: str = "run" # run|session|global
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
object.__setattr__(self, "note", str(self.note or "").strip())
|
|
28
|
+
object.__setattr__(self, "tags", dict(self.tags or {}))
|
|
29
|
+
raw_scope = str(getattr(self, "scope", "") or "").strip().lower() or "run"
|
|
30
|
+
if raw_scope not in ("run", "session", "global"):
|
|
31
|
+
raw_scope = "run"
|
|
32
|
+
object.__setattr__(self, "scope", raw_scope)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_remember_args(raw: str) -> RememberRequest:
|
|
36
|
+
"""Parse `/memorize` arguments.
|
|
37
|
+
|
|
38
|
+
Syntax:
|
|
39
|
+
/memorize <note text> [--tag k=v ...] [--span <span_id>] [--last-span] [--last N] [--scope run|session|global]
|
|
40
|
+
|
|
41
|
+
Notes:
|
|
42
|
+
- Note text may be quoted, but quoting is optional (we treat all non-flag tokens as note text).
|
|
43
|
+
- Tags are JSON-safe `str -> str` and `kind=...` is ignored (reserved).
|
|
44
|
+
"""
|
|
45
|
+
import shlex
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
parts = shlex.split(raw) if raw else []
|
|
49
|
+
except ValueError:
|
|
50
|
+
parts = raw.split() if raw else []
|
|
51
|
+
|
|
52
|
+
tags: Dict[str, str] = {}
|
|
53
|
+
span_id: Optional[str] = None
|
|
54
|
+
last_span = False
|
|
55
|
+
last_messages = 6
|
|
56
|
+
scope = "run"
|
|
57
|
+
note_parts: list[str] = []
|
|
58
|
+
|
|
59
|
+
i = 0
|
|
60
|
+
while i < len(parts):
|
|
61
|
+
p = str(parts[i])
|
|
62
|
+
if p in ("--tag", "--tags"):
|
|
63
|
+
if i + 1 >= len(parts):
|
|
64
|
+
raise ValueError("--tag requires k=v")
|
|
65
|
+
kv = str(parts[i + 1])
|
|
66
|
+
if "=" not in kv:
|
|
67
|
+
raise ValueError("--tag requires k=v")
|
|
68
|
+
k, v = kv.split("=", 1)
|
|
69
|
+
key = k.strip()
|
|
70
|
+
val = v.strip()
|
|
71
|
+
if not key or not val:
|
|
72
|
+
raise ValueError("--tag requires non-empty k=v")
|
|
73
|
+
if key != "kind":
|
|
74
|
+
tags[key] = val
|
|
75
|
+
i += 2
|
|
76
|
+
continue
|
|
77
|
+
if p == "--span":
|
|
78
|
+
if i + 1 >= len(parts):
|
|
79
|
+
raise ValueError("--span requires a span_id")
|
|
80
|
+
span_id = str(parts[i + 1]).strip() or None
|
|
81
|
+
i += 2
|
|
82
|
+
continue
|
|
83
|
+
if p == "--last-span":
|
|
84
|
+
last_span = True
|
|
85
|
+
i += 1
|
|
86
|
+
continue
|
|
87
|
+
if p == "--last":
|
|
88
|
+
if i + 1 >= len(parts):
|
|
89
|
+
raise ValueError("--last requires a number")
|
|
90
|
+
try:
|
|
91
|
+
last_messages = int(parts[i + 1])
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ValueError("--last requires a number") from e
|
|
94
|
+
if last_messages < 0:
|
|
95
|
+
last_messages = 0
|
|
96
|
+
i += 2
|
|
97
|
+
continue
|
|
98
|
+
if p == "--scope":
|
|
99
|
+
if i + 1 >= len(parts):
|
|
100
|
+
raise ValueError("--scope requires a value")
|
|
101
|
+
scope = str(parts[i + 1]).strip().lower() or "run"
|
|
102
|
+
if scope not in ("run", "session", "global"):
|
|
103
|
+
raise ValueError("--scope must be run|session|global")
|
|
104
|
+
i += 2
|
|
105
|
+
continue
|
|
106
|
+
if p.startswith("--"):
|
|
107
|
+
raise ValueError(f"Unknown flag: {p}")
|
|
108
|
+
|
|
109
|
+
note_parts.append(p)
|
|
110
|
+
i += 1
|
|
111
|
+
|
|
112
|
+
note = " ".join([p for p in note_parts if p]).strip()
|
|
113
|
+
if not note:
|
|
114
|
+
raise ValueError("note text is required")
|
|
115
|
+
|
|
116
|
+
return RememberRequest(note=note, tags=tags, span_id=span_id, last_span=last_span, last_messages=last_messages, scope=scope)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def store_memory_note(
|
|
120
|
+
*,
|
|
121
|
+
runtime: Any,
|
|
122
|
+
target_run_id: str,
|
|
123
|
+
note: str,
|
|
124
|
+
tags: Dict[str, str],
|
|
125
|
+
sources: Dict[str, Any],
|
|
126
|
+
actor_id: Optional[str],
|
|
127
|
+
session_id: Optional[str],
|
|
128
|
+
call_id: str = "memorize",
|
|
129
|
+
scope: str = "run",
|
|
130
|
+
) -> Dict[str, Any]:
|
|
131
|
+
"""Store a runtime memory note targeting `target_run_id`.
|
|
132
|
+
|
|
133
|
+
This is implemented as a tiny child workflow that emits `EffectType.MEMORY_NOTE`
|
|
134
|
+
with `payload.target_run_id`, so the runtime stores the note on the target run.
|
|
135
|
+
"""
|
|
136
|
+
from abstractruntime import Effect, EffectType, StepPlan, WorkflowSpec
|
|
137
|
+
from abstractruntime.core.models import RunStatus
|
|
138
|
+
|
|
139
|
+
payload: Dict[str, Any] = {
|
|
140
|
+
"target_run_id": str(target_run_id),
|
|
141
|
+
"note": str(note or ""),
|
|
142
|
+
"tags": dict(tags or {}),
|
|
143
|
+
"sources": dict(sources or {}),
|
|
144
|
+
"scope": str(scope or "run"),
|
|
145
|
+
"tool_name": "remember_note",
|
|
146
|
+
"call_id": str(call_id or "remember"),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def remember_node(run, ctx) -> StepPlan:
|
|
150
|
+
return StepPlan(
|
|
151
|
+
node_id="remember",
|
|
152
|
+
effect=Effect(type=EffectType.MEMORY_NOTE, payload=payload, result_key="_temp.remember"),
|
|
153
|
+
next_node="done",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def done_node(run, ctx) -> StepPlan:
|
|
157
|
+
temp = run.vars.get("_temp")
|
|
158
|
+
if not isinstance(temp, dict):
|
|
159
|
+
temp = {}
|
|
160
|
+
return StepPlan(node_id="done", complete_output={"result": temp.get("remember")})
|
|
161
|
+
|
|
162
|
+
wf = WorkflowSpec(
|
|
163
|
+
workflow_id="abstractcode_remember_command",
|
|
164
|
+
entry_node="remember",
|
|
165
|
+
nodes={"remember": remember_node, "done": done_node},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
remember_run_id = runtime.start(
|
|
169
|
+
workflow=wf,
|
|
170
|
+
vars={"context": {}, "scratchpad": {}, "_runtime": {}, "_temp": {}, "_limits": {}},
|
|
171
|
+
actor_id=actor_id,
|
|
172
|
+
session_id=session_id,
|
|
173
|
+
parent_run_id=str(target_run_id),
|
|
174
|
+
)
|
|
175
|
+
st = runtime.tick(workflow=wf, run_id=remember_run_id, max_steps=50)
|
|
176
|
+
if st.status != RunStatus.COMPLETED:
|
|
177
|
+
raise RuntimeError(st.error or "remember_note failed")
|
|
178
|
+
|
|
179
|
+
out = st.output or {}
|
|
180
|
+
result = out.get("result") if isinstance(out, dict) else None
|
|
181
|
+
if not isinstance(result, dict):
|
|
182
|
+
return {"remember_run_id": remember_run_id, "result": result}
|
|
183
|
+
return {"remember_run_id": remember_run_id, **result}
|
|
184
|
+
|