ltcai 11.5.0 → 11.5.1

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.
Files changed (77) hide show
  1. package/README.md +25 -18
  2. package/docs/CHANGELOG.md +22 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/OPERATIONS.md +1 -1
  7. package/docs/TRUST_MODEL.md +1 -1
  8. package/docs/WHY_LATTICE.md +1 -1
  9. package/docs/kg-schema.md +1 -1
  10. package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/runtime/multi_agent.py +1 -1
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/agent_worker_seam.py +389 -0
  15. package/latticeai/core/legacy_compatibility.py +1 -1
  16. package/latticeai/core/marketplace.py +1 -1
  17. package/latticeai/core/messages.py +35 -0
  18. package/latticeai/core/workspace_os_constants.py +1 -1
  19. package/latticeai/runtime/build_phases/features.py +23 -2
  20. package/latticeai/services/architecture_readiness.py +1 -1
  21. package/latticeai/services/product_readiness.py +1 -1
  22. package/package.json +1 -1
  23. package/scripts/check_current_release_docs.mjs +1 -1
  24. package/scripts/check_server_i18n.mjs +1 -0
  25. package/scripts/generate_agent_loop_fixtures.py +907 -0
  26. package/scripts/generate_rust_parity_fixtures.py +70 -67
  27. package/scripts/parity_fixture_corpus_docgen.py +341 -0
  28. package/scripts/release_screen_claims.json +10 -0
  29. package/src-tauri/Cargo.lock +13 -7
  30. package/src-tauri/Cargo.toml +1 -1
  31. package/src-tauri/tauri.conf.json +1 -1
  32. package/static/app/asset-manifest.json +41 -41
  33. package/static/app/assets/{Act-CWnxSCgN.js → Act-Drs_jd-O.js} +1 -1
  34. package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-BqNx5oF6.js} +1 -1
  35. package/static/app/assets/{Brain-DWu1BhFg.js → Brain-C7bdNmz-.js} +1 -1
  36. package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-DcTnINcI.js} +1 -1
  37. package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-Df1MRIPD.js} +1 -1
  38. package/static/app/assets/{Capture-BHpCxnzb.js → Capture-Dgsxvnbx.js} +1 -1
  39. package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BrkTxJK1.js} +1 -1
  40. package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-DDziw7Oj.js} +1 -1
  41. package/static/app/assets/{Library-DgYcHome.js → Library-CYxeuraA.js} +1 -1
  42. package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-CNp6mvCm.js} +1 -1
  43. package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-BVsowu3Z.js} +1 -1
  44. package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-D1LbOfJS.js} +1 -1
  45. package/static/app/assets/{System-NF8IfhTa.js → System-DcRAq7wj.js} +1 -1
  46. package/static/app/assets/arrow-left-BHYmOTWU.js +1 -0
  47. package/static/app/assets/{bot-CucuhLhm.js → bot-BK2xQ9mN.js} +1 -1
  48. package/static/app/assets/{brain-BBnSryW_.js → brain-BZNztFBb.js} +1 -1
  49. package/static/app/assets/{button-C2GUj2Ai.js → button-CjueubVZ.js} +1 -1
  50. package/static/app/assets/circle-check-CLeWJr67.js +1 -0
  51. package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-HRjSmpyY.js} +1 -1
  52. package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-CMcKDVwu.js} +1 -1
  53. package/static/app/assets/{cpu-DAlCXlIy.js → cpu-B_MyTfYr.js} +1 -1
  54. package/static/app/assets/{download-RNhuuJwh.js → download-CTy0EByV.js} +1 -1
  55. package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-DF6bLhTg.js} +1 -1
  56. package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-CpWHG77C.js} +1 -1
  57. package/static/app/assets/{index-DMurvUuR.js → index-Dd6abJHX.js} +3 -3
  58. package/static/app/assets/index-DxmOfNRi.css +2 -0
  59. package/static/app/assets/{input-D2UhPC1X.js → input-bVgIRN3s.js} +1 -1
  60. package/static/app/assets/{link-2-6amKbP_P.js → link-2-bg6CG5Vk.js} +1 -1
  61. package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-DFTf1HDZ.js} +1 -1
  62. package/static/app/assets/{primitives-gPsccucr.js → primitives-D7D-sQ3T.js} +1 -1
  63. package/static/app/assets/search-BgSVX6NG.js +1 -0
  64. package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BLzo7U4L.js} +1 -1
  65. package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-BmngqnsJ.js} +1 -1
  66. package/static/app/assets/{textarea-BQnVWhYs.js → textarea-DOSlAQQ3.js} +1 -1
  67. package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-pZhgeee4.js} +1 -1
  68. package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-BMDwNk4I.js} +1 -1
  69. package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CSjKttRo.js} +1 -1
  70. package/static/app/assets/{utils-V_5-wxr5.js → utils-D3u_yv7B.js} +1 -1
  71. package/static/app/assets/{workspace-K1zjYUHj.js → workspace-H3bMjYXC.js} +1 -1
  72. package/static/app/index.html +4 -4
  73. package/static/sw.js +1 -1
  74. package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
  75. package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
  76. package/static/app/assets/index-BLPb5lmE.css +0 -2
  77. package/static/app/assets/search-Cj_TKk_2.js +0 -1
@@ -0,0 +1,389 @@
1
+ """AI-Worker seam (v11.5.1, plan §Y1) — the three calls the Rust loop makes back.
2
+
3
+ Once the agent loop moves into ``lattice-agent`` (plan §Y2), Python stops being
4
+ the orchestrator and becomes exactly what the system diagram already draws: the
5
+ **AI Worker**. It infers with the loaded model, it runs tool handlers, and it
6
+ stages proposals. The Rust kernel decides *what* to do next; it has to call
7
+ Python to actually do it.
8
+
9
+ Reconnaissance found no surface it could call:
10
+
11
+ * there is no bare completion endpoint — every LLM route (``/chat``,
12
+ ``/agent``) also writes history, assembles context, or drives the whole
13
+ Python loop, none of which a Rust orchestrator wants;
14
+ * HTTP cannot create a change proposal at all — ``ChangeProposalService.review``
15
+ is reachable only from the in-process agent runtime;
16
+ * ``/tools/*`` is the **direct** surface: it runs ``enforce_policy``, which
17
+ denies anything not auto-approved (403) and never stages a proposal. Correct
18
+ for a human clicking a button, useless for a governed loop.
19
+
20
+ So this module adds the three seams, and nothing else:
21
+
22
+ ``POST /agent/llm``
23
+ One completion. No history, no context assembly, no persistence of any
24
+ kind — the whole body is one ``generate_as`` await.
25
+
26
+ ``POST /agent/tool``
27
+ One governed tool call: the mode-invariant guards, the role check, then the
28
+ shared ``pre_tool`` → execute → ``post_tool`` lifecycle.
29
+
30
+ ``POST /agent/change-proposal``
31
+ The governor's verdict, verbatim, so the Rust loop can take the
32
+ proposal-first path for edits to existing files.
33
+
34
+ Two boundaries stated here so the payloads are not read as more than they are:
35
+
36
+ * **The guards are re-run on the server, always.** The Rust kernel preflights
37
+ permission mode before it ever calls; this seam still re-derives the policy,
38
+ still asks :func:`~latticeai.core.permission_mode.is_circuit_breaker`, and
39
+ still asks :func:`~latticeai.core.tool_governor.classify_tool_call`. A
40
+ compromised or buggy kernel therefore cannot widen what Python will execute —
41
+ the mode-invariant denials are defence in depth, not a duplicated preflight.
42
+ What the seam deliberately does *not* own is mode gating itself (which of the
43
+ approval-requiring steps may run): that is the kernel's decision, made with
44
+ the run's approval state, which HTTP does not have.
45
+
46
+ * **``workspace_id`` is attribution, not authorization.** Tools resolve their
47
+ own paths under ``AGENT_ROOT`` (or the home sandbox, via their own guards);
48
+ they are not workspace-scoped resources the way ``/api/*`` reads are. The
49
+ field is forwarded to the hook lifecycle so an audit event lands in the right
50
+ workspace, and it is not used to widen or narrow what may run. A caller
51
+ cannot reach another workspace's data by naming it here, because no code path
52
+ downstream consults it for that.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import asyncio
58
+ import os
59
+ from typing import Any, Callable, Dict, Optional
60
+
61
+ from fastapi import APIRouter, HTTPException, Request
62
+ from pydantic import BaseModel, Field
63
+
64
+ from lattice_brain.runtime.hooks import dispatch_tool
65
+ from latticeai.core.messages import http_error, resolve_language
66
+ from latticeai.core.permission_mode import is_circuit_breaker
67
+ from latticeai.core.tool_governor import classify_tool_call
68
+ from latticeai.tools import ToolError
69
+
70
+ #: Host-injected switch. Off by default: the loop seam is for a worker the
71
+ #: ``lattice-host`` supervisor started for itself, never for a browser that
72
+ #: happens to hold a session cookie. Read per request, not at import, so the
73
+ #: answer follows the process environment as it actually is.
74
+ SEAM_ENV_VAR = "LATTICEAI_AGENT_TOOL_SEAM"
75
+
76
+ #: Bounds on one completion. The ceiling is twice ``generate_as``'s own default
77
+ #: — enough for a long verification pass, short of a request that would hold the
78
+ #: single MLX executor for minutes.
79
+ MIN_MAX_TOKENS = 1
80
+ MAX_MAX_TOKENS = 8192
81
+ MIN_TEMPERATURE = 0.0
82
+ MAX_TEMPERATURE = 2.0
83
+
84
+ #: Rate-limit bucket. Deliberately *not* the ``"agent"`` bucket ``/agent`` uses:
85
+ #: that one is sized per *run* (10 burst, one refill per 10s) because one HTTP
86
+ #: call there is a whole agent run. Here one call is a single loop step, and a
87
+ #: Rust run makes a dozen of them, so reusing ``"agent"`` would 429 the loop
88
+ #: mid-run. This key is absent from ``_RATE_LIMITS``, so it takes the module
89
+ #: default (60 burst, 1/s) — a real per-user ceiling at per-step granularity.
90
+ SEAM_RATE_BUCKET = "agent_seam"
91
+
92
+
93
+ class AgentLLMRequest(BaseModel):
94
+ """One completion, with the model chosen per call.
95
+
96
+ ``model_id`` omitted means the router's current default; a model that is
97
+ not cached makes ``generate_as`` answer ``"No model."``, which is returned
98
+ verbatim rather than dressed up as an error — the loop records it as the
99
+ step's text and re-plans, exactly as the Python loop does.
100
+ """
101
+
102
+ model_id: Optional[str] = None
103
+ message: str
104
+ context: Optional[str] = None
105
+ max_tokens: int = 4096
106
+ temperature: float = 0.2
107
+
108
+
109
+ class AgentToolRequest(BaseModel):
110
+ """One governed tool call on behalf of the authenticated user."""
111
+
112
+ tool: str
113
+ args: Dict[str, Any] = Field(default_factory=dict)
114
+ workspace_id: Optional[str] = None
115
+
116
+
117
+ class AgentChangeProposalRequest(BaseModel):
118
+ """A governor consultation for a write that may touch existing content.
119
+
120
+ ``policy`` is optional: the Rust kernel already holds the policy it
121
+ preflighted with, and passing it back keeps the two sides deciding on the
122
+ same facts. Omitted, the registry's own policy for this call is used.
123
+ """
124
+
125
+ tool: str
126
+ args: Dict[str, Any] = Field(default_factory=dict)
127
+ policy: Optional[Dict[str, Any]] = None
128
+ workspace_id: Optional[str] = None
129
+ conversation_id: Optional[str] = None
130
+
131
+
132
+ def _seam_open() -> bool:
133
+ """Whether this process is a worker the host opened the seam for."""
134
+ return os.environ.get(SEAM_ENV_VAR) == "1"
135
+
136
+
137
+ def _path_probe(
138
+ dispatch_service: Any, tool: str
139
+ ) -> Optional[Callable[[str], bool]]:
140
+ """The *same* existence probe the direct surface classifies with.
141
+
142
+ ``classify_tool_call`` asks "does the target already exist?" to tell an
143
+ additive create from an overwrite. ``ToolDispatchService`` answers that
144
+ through ``_governed_path_exists``, which resolves document creators'
145
+ ``filename`` through their real output directory first — checking the raw
146
+ argument inspects a path nothing ever writes. Reusing that method is the
147
+ point: a second, subtly different probe here would let this seam and
148
+ ``/tools/*`` disagree about whether a file exists, and the disagreement
149
+ would show up as one surface staging a proposal while the other overwrites.
150
+
151
+ ``classify_tool_call`` types ``path_exists`` as optional, so a dispatch
152
+ service without the method (an injected fake, a future slimmer port) yields
153
+ ``None`` and every target-write call classifies as additive. That is the
154
+ weaker guard, and it is the honest one: claiming a file does not exist is
155
+ what "we could not look" means to this classifier.
156
+ """
157
+ probe = getattr(dispatch_service, "_governed_path_exists", None)
158
+ if not callable(probe):
159
+ return None
160
+ return lambda candidate: bool(probe(tool, candidate))
161
+
162
+
163
+ def create_agent_worker_seam_router(
164
+ *,
165
+ model_router: Any,
166
+ dispatch_service: Any,
167
+ execute_tool: Callable[[str, Dict[str, Any]], Any],
168
+ hooks: Any,
169
+ change_proposals: Any,
170
+ require_user: Callable[[Request], Any],
171
+ enforce_rate_limit: Callable[[str, str], None],
172
+ ) -> APIRouter:
173
+ router = APIRouter()
174
+
175
+ def _require_seam(request: Request) -> None:
176
+ """404 unless the host opened the seam for this worker.
177
+
178
+ The detail says *why* rather than imitating a missing route. Hiding it
179
+ would buy nothing: the paths are in the generated OpenAPI schema either
180
+ way, this is a local-first Brain rather than a multi-tenant service, and
181
+ an operator who forgot the environment variable otherwise gets a bare
182
+ 404 with nothing to act on.
183
+ """
184
+ if not _seam_open():
185
+ raise http_error(404, "agent_seam.disabled", resolve_language(request))
186
+
187
+ def _admit(request: Request) -> str:
188
+ """Authenticate and charge this call against the per-step budget."""
189
+ current_user = require_user(request)
190
+ enforce_rate_limit(current_user, SEAM_RATE_BUCKET)
191
+ return str(current_user or "")
192
+
193
+ def _guard(tool: str, args: Dict[str, Any], language: str) -> Dict[str, Any]:
194
+ """The mode-invariant denials, re-derived server-side.
195
+
196
+ One call to :func:`is_circuit_breaker` covers both denials the plan
197
+ names. The destructive-policy check is *inside* it — ``permission_mode``
198
+ answers ``"destructive action is always blocked"`` for
199
+ ``policy["destructive"]`` or ``risk == "destructive"`` before it looks
200
+ at anything else. Writing a second, separate destructive check here (as
201
+ ``enforce_policy`` does) would be code that can never run, and
202
+ unreachable code is not a guard.
203
+ """
204
+ policy = dispatch_service.policy_for(tool, args)
205
+ breaker = is_circuit_breaker(tool, dict(policy), args)
206
+ if breaker:
207
+ raise http_error(
208
+ 403,
209
+ "agent_seam.tool_blocked",
210
+ language,
211
+ tool=tool,
212
+ reason=breaker,
213
+ )
214
+ verdict = classify_tool_call(
215
+ tool,
216
+ args,
217
+ policy=dict(policy),
218
+ path_exists=_path_probe(dispatch_service, tool),
219
+ )
220
+ if verdict.get("fail_closed"):
221
+ raise http_error(
222
+ 409,
223
+ "agent_seam.tool_fail_closed",
224
+ language,
225
+ tool=tool,
226
+ reason=str(verdict.get("reason") or ""),
227
+ )
228
+ return dict(policy)
229
+
230
+ @router.post("/agent/llm")
231
+ async def agent_llm(req: AgentLLMRequest, request: Request):
232
+ """Generate once. Writes nothing, remembers nothing.
233
+
234
+ Not gated by ``LATTICEAI_AGENT_TOOL_SEAM``: a completion has no side
235
+ effect to gate, and the Rust loop is not the only caller that wants one
236
+ (``/rust/context/document`` composes a prompt the same way). Auth and
237
+ the per-step rate limit are the whole ceremony.
238
+
239
+ Structural problems in the body — a missing ``message``, a string where
240
+ a number belongs — answer with FastAPI's own 422, uniform with every
241
+ other router. Semantic ones answer in the caller's language.
242
+ """
243
+ _admit(request)
244
+ language = resolve_language(request)
245
+ if not req.message.strip():
246
+ raise http_error(422, "agent_seam.message_required", language)
247
+ if req.max_tokens < MIN_MAX_TOKENS or req.max_tokens > MAX_MAX_TOKENS:
248
+ raise http_error(
249
+ 422,
250
+ "agent_seam.max_tokens_out_of_range",
251
+ language,
252
+ min=MIN_MAX_TOKENS,
253
+ max=MAX_MAX_TOKENS,
254
+ )
255
+ if req.temperature < MIN_TEMPERATURE or req.temperature > MAX_TEMPERATURE:
256
+ raise http_error(
257
+ 422,
258
+ "agent_seam.temperature_out_of_range",
259
+ language,
260
+ min=MIN_TEMPERATURE,
261
+ max=MAX_TEMPERATURE,
262
+ )
263
+ text = await model_router.generate_as(
264
+ req.model_id or None,
265
+ message=req.message,
266
+ context=req.context,
267
+ max_tokens=req.max_tokens,
268
+ temperature=req.temperature,
269
+ )
270
+ return {"text": str(text)}
271
+
272
+ @router.post("/agent/tool")
273
+ async def agent_tool(req: AgentToolRequest, request: Request):
274
+ """Run one tool through the same lifecycle the Python loop uses.
275
+
276
+ The answer shape mirrors the loop's own catch (``execution.py``): a
277
+ ``ToolError``/``KeyError``/``TypeError``/``PermissionError`` is the
278
+ *step's* outcome, not the request's, so it comes back 200 with
279
+ ``{"error": ...}`` for the transcript. A denial — role, circuit
280
+ breaker, fail-closed governance — is the request's outcome and comes
281
+ back 4xx, because retrying it would be pointless.
282
+ """
283
+ _require_seam(request)
284
+ current_user = _admit(request)
285
+ language = resolve_language(request)
286
+ tool = req.tool.strip()
287
+ if not tool:
288
+ raise http_error(422, "agent_seam.tool_required", language)
289
+ args = dict(req.args)
290
+
291
+ _guard(tool, args, language)
292
+
293
+ # After the mode-invariant denials, deliberately: they are the same
294
+ # answer for every role, so they need no user-table read to decide.
295
+ try:
296
+ dispatch_service.check_role(tool, current_user)
297
+ except PermissionError as exc:
298
+ # The shipped service raises HTTPException(403) here and that
299
+ # propagates untouched; a port that speaks Python's own
300
+ # authorization error gets the same status rather than a 500.
301
+ raise HTTPException(status_code=403, detail=str(exc)) from exc
302
+
303
+ def _run() -> Any:
304
+ return dispatch_tool(
305
+ hooks,
306
+ tool,
307
+ args,
308
+ lambda: execute_tool(tool, args),
309
+ user_email=current_user,
310
+ workspace_id=req.workspace_id,
311
+ source="agent",
312
+ )
313
+
314
+ try:
315
+ # Off the loop: tool handlers open files, shell out, and write the
316
+ # graph, and this server has one event loop for every user (10.9.0).
317
+ result = await asyncio.to_thread(_run)
318
+ except (ToolError, KeyError, TypeError, PermissionError) as exc:
319
+ return {"error": str(exc)}
320
+ return {"result": result}
321
+
322
+ @router.post("/agent/change-proposal")
323
+ async def agent_change_proposal(
324
+ req: AgentChangeProposalRequest, request: Request
325
+ ):
326
+ """Ask the governor what should happen to this write.
327
+
328
+ The verdict is returned verbatim — ``{"decision": "allow_additive"}``
329
+ or ``{"decision": "proposed", "proposal": {...}}`` — because the Rust
330
+ loop has to act on the same facts the Review Center will show.
331
+
332
+ ``review`` answers ``None`` for "I have nothing to say about this call:
333
+ fall through to the normal gates". Three different situations collapse
334
+ into that one ``None`` (not a governed tool; no proposal required; the
335
+ edit could not be computed deterministically) and the service does not
336
+ distinguish them, so neither does this payload: ``{"decision": "none"}``
337
+ and nothing invented about why.
338
+
339
+ No mode-invariant guard here, because nothing executes: staging a
340
+ proposal writes a review item, and the write itself still has to come
341
+ back through ``/agent/tool`` — where the guards are.
342
+ """
343
+ _require_seam(request)
344
+ current_user = _admit(request)
345
+ language = resolve_language(request)
346
+ if change_proposals is None:
347
+ raise http_error(503, "agent_seam.proposals_unavailable", language)
348
+ tool = req.tool.strip()
349
+ if not tool:
350
+ raise http_error(422, "agent_seam.tool_required", language)
351
+ args = dict(req.args)
352
+ policy = (
353
+ dict(req.policy)
354
+ if req.policy is not None
355
+ else dict(dispatch_service.policy_for(tool, args))
356
+ )
357
+
358
+ def _review() -> Optional[Dict[str, Any]]:
359
+ return change_proposals.review(
360
+ tool,
361
+ args,
362
+ policy=policy,
363
+ user_email=current_user,
364
+ workspace_id=req.workspace_id,
365
+ conversation_id=req.conversation_id,
366
+ )
367
+
368
+ # Staging reads the target, computes a diff, and writes a review item —
369
+ # all blocking I/O, none of it belonging on the event loop.
370
+ verdict = await asyncio.to_thread(_review)
371
+ if verdict is None:
372
+ return {"decision": "none"}
373
+ return verdict
374
+
375
+ return router
376
+
377
+
378
+ __all__ = [
379
+ "MAX_MAX_TOKENS",
380
+ "MAX_TEMPERATURE",
381
+ "MIN_MAX_TOKENS",
382
+ "MIN_TEMPERATURE",
383
+ "SEAM_ENV_VAR",
384
+ "SEAM_RATE_BUCKET",
385
+ "AgentChangeProposalRequest",
386
+ "AgentLLMRequest",
387
+ "AgentToolRequest",
388
+ "create_agent_worker_seam_router",
389
+ ]
@@ -13,7 +13,7 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import Any, Dict, List
15
15
 
16
- LEGACY_COMPATIBILITY_VERSION = "11.5.0"
16
+ LEGACY_COMPATIBILITY_VERSION = "11.5.1"
17
17
 
18
18
 
19
19
  @dataclass(frozen=True)
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
- MARKETPLACE_VERSION = "11.5.0"
13
+ MARKETPLACE_VERSION = "11.5.1"
14
14
  TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
15
15
 
16
16
 
@@ -536,6 +536,41 @@ MESSAGES: Dict[str, Dict[str, str]] = {
536
536
  "ko": "한 번에 처리할 개수는 {min}에서 {max} 사이여야 합니다.",
537
537
  "en": "Ask for between {min} and {max} items in one pass.",
538
538
  },
539
+ # ── AI-Worker seam (the Rust loop's three calls back into Python) ────
540
+ "agent_seam.disabled": {
541
+ "ko": "워커 시임이 꺼져 있습니다. 호스트가 직접 띄운 워커에서만 열립니다.",
542
+ "en": "The worker seam is off. It opens only in a worker the host started itself.",
543
+ },
544
+ "agent_seam.message_required": {
545
+ "ko": "모델에 보낼 내용을 적어주세요.",
546
+ "en": "Write the message to send to the model.",
547
+ },
548
+ "agent_seam.max_tokens_out_of_range": {
549
+ "ko": "한 번에 만들 길이는 {min}에서 {max} 사이여야 합니다.",
550
+ "en": "Ask for between {min} and {max} tokens in one completion.",
551
+ },
552
+ "agent_seam.temperature_out_of_range": {
553
+ "ko": "온도는 {min}에서 {max} 사이여야 합니다.",
554
+ "en": "Temperature has to be between {min} and {max}.",
555
+ },
556
+ "agent_seam.tool_required": {
557
+ "ko": "실행할 도구 이름이 필요합니다.",
558
+ "en": "A tool name to run is required.",
559
+ },
560
+ "agent_seam.tool_blocked": {
561
+ "ko": "'{tool}' 도구는 어떤 모드에서도 차단됩니다 ({reason}).",
562
+ "en": "'{tool}' is blocked in every mode ({reason}).",
563
+ },
564
+ "agent_seam.tool_fail_closed": {
565
+ "ko": "'{tool}' 도구는 기존 내용을 바꾸지만 검토할 수 있는 제안으로 "
566
+ "만들 수 없어 차단했습니다 ({reason}).",
567
+ "en": "'{tool}' would change existing content but cannot be staged as a "
568
+ "reviewable proposal, so it is blocked ({reason}).",
569
+ },
570
+ "agent_seam.proposals_unavailable": {
571
+ "ko": "변경 제안 서비스가 연결되어 있지 않습니다.",
572
+ "en": "The change proposal service is not connected.",
573
+ },
539
574
  # ── MCP / setup ─────────────────────────────────────────────────────
540
575
  "mcp.connector_not_found": {
541
576
  "ko": "커넥터를 찾을 수 없습니다.",
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from typing import Dict
12
12
 
13
- WORKSPACE_OS_VERSION = "11.5.0"
13
+ WORKSPACE_OS_VERSION = "11.5.1"
14
14
 
15
15
  # Workspace types separate single-user Personal workspaces from shared
16
16
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -16,6 +16,7 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
16
16
  """Workspace platform, automation, review queue, command centre, proposals."""
17
17
  ctx.enter("platform_features")
18
18
 
19
+ from latticeai.api.agent_worker_seam import create_agent_worker_seam_router
19
20
  from latticeai.api.agents import create_agents_router
20
21
  from latticeai.api.automation_intelligence import (
21
22
  create_automation_intelligence_router,
@@ -48,9 +49,12 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
48
49
  from latticeai.services.chronicle import ChronicleService
49
50
  from latticeai.services.command_center import CommandCenterService
50
51
  from latticeai.services.evidence_actions import EvidenceActionService
51
- from latticeai.services.tool_dispatch import get_tool_permission
52
+ from latticeai.services.tool_dispatch import (
53
+ DEFAULT_TOOL_DISPATCH_SERVICE,
54
+ get_tool_permission,
55
+ )
52
56
  from latticeai.services.voice_capture import VoiceCaptureService
53
- from latticeai.tools import resolve_workspace_path
57
+ from latticeai.tools import execute_tool, resolve_workspace_path
54
58
 
55
59
  # v2 Agentic Workspace Platform: cross-system wiring.
56
60
  platform_automation_runtime = build_platform_automation_runtime(
@@ -210,6 +214,23 @@ def phase_platform_features(ctx: RuntimeContext) -> None:
210
214
  )
211
215
  )
212
216
 
217
+ # AI-Worker seam (v11.5.1, plan §Y1): the three calls the Rust agent loop
218
+ # makes back into Python once orchestration lives in ``lattice-agent``.
219
+ # Registered here because it needs the change governor above; the two
220
+ # side-effecting routes stay behind ``LATTICEAI_AGENT_TOOL_SEAM=1``, which
221
+ # only lattice-host injects into a worker it started.
222
+ ctx.app.include_router(
223
+ create_agent_worker_seam_router(
224
+ model_router=ctx.model_router,
225
+ dispatch_service=DEFAULT_TOOL_DISPATCH_SERVICE,
226
+ execute_tool=execute_tool,
227
+ hooks=ctx.HOOKS_REGISTRY,
228
+ change_proposals=ctx.CHANGE_PROPOSALS,
229
+ require_user=ctx.require_user,
230
+ enforce_rate_limit=ctx.enforce_rate_limit,
231
+ )
232
+ )
233
+
213
234
  # Evidence → action (v9.9.6): an answer's citations become ready-to-send,
214
235
  # evidence-scoped follow-ups. Deterministic composition only — execution
215
236
  # stays on the chat/file-generation path.
@@ -16,7 +16,7 @@ from typing import Any, Dict, List
16
16
 
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
- ARCHITECTURE_VERSION_TARGET = "11.5.0"
19
+ ARCHITECTURE_VERSION_TARGET = "11.5.1"
20
20
 
21
21
  PREFERRED_REFACTORING_ORDER = [
22
22
  "agent-runtime",
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
18
18
 
19
19
  from latticeai.services.architecture_readiness import architecture_readiness
20
20
 
21
- PRODUCT_VERSION_TARGET = "11.5.0"
21
+ PRODUCT_VERSION_TARGET = "11.5.1"
22
22
 
23
23
 
24
24
  @dataclass(frozen=True)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "11.5.0",
3
+ "version": "11.5.1",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -6,7 +6,7 @@ const root = process.cwd();
6
6
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
7
7
  const version = pkg.version;
8
8
  const releaseDir = `output/release/v${version}`;
9
- const releaseTheme = "Rust Complete";
9
+ const releaseTheme = "Rust Full Loop";
10
10
  const title = `${version} — ${releaseTheme}`;
11
11
  const escapedVersion = version.replaceAll(".", "\\.");
12
12
 
@@ -29,6 +29,7 @@ const API_DIR = "latticeai/api";
29
29
  // which is how the removed root shims used to be referenced.
30
30
  const LOCALIZED = [
31
31
  "admin",
32
+ "agent_worker_seam",
32
33
  "auth",
33
34
  "browser",
34
35
  "chat",