ltcai 9.6.0 → 9.8.0
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.
- package/README.md +98 -306
- package/auto_setup.py +11 -1
- package/docs/CHANGELOG.md +91 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/PERFORMANCE.md +106 -0
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/kg_schema.py +11 -1
- package/knowledge_graph.py +12 -2
- package/knowledge_graph_api.py +11 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +583 -0
- package/lattice_brain/graph/retrieval.py +301 -8
- package/lattice_brain/graph/retrieval_vector.py +145 -0
- package/lattice_brain/ingestion.py +757 -16
- package/lattice_brain/quality.py +44 -9
- package/lattice_brain/runtime/multi_agent.py +38 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +39 -2
- package/latticeai/api/change_proposals.py +32 -3
- package/latticeai/api/chat.py +17 -0
- package/latticeai/api/chat_helpers.py +48 -0
- package/latticeai/api/chat_stream.py +9 -1
- package/latticeai/api/local_files.py +120 -2
- package/latticeai/api/review_queue.py +66 -2
- package/latticeai/app_factory.py +3 -0
- package/latticeai/core/agent.py +193 -135
- package/latticeai/core/agent_eval.py +278 -4
- package/latticeai/core/legacy_compatibility.py +15 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/router_registration.py +2 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_intelligence.py +151 -14
- package/latticeai/services/brain_intelligence.py +169 -1
- package/latticeai/services/change_proposals.py +56 -4
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/review_queue.py +43 -2
- package/llm_router.py +10 -1
- package/local_knowledge_api.py +10 -1
- package/ltcai_cli.py +11 -1
- package/mcp_registry.py +10 -1
- package/p_reinforce.py +10 -1
- package/package.json +1 -1
- package/scripts/brain_quality_eval.py +1 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/profile_kg.py +360 -0
- package/setup_wizard.py +10 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/{Act-BkOEmwBi.js → Act-Dd3z8AzF.js} +2 -1
- package/static/app/assets/Brain-BMkgdWnI.js +321 -0
- package/static/app/assets/Capture-D2Aw9gkv.js +1 -0
- package/static/app/assets/Library-Yreq-KW5.js +1 -0
- package/static/app/assets/System-CXNmmtEo.js +1 -0
- package/static/app/assets/{index-85wQvEie.css → index-7gY9t9Sd.css} +1 -1
- package/static/app/assets/index-CndfILiF.js +18 -0
- package/static/app/assets/primitives-DxsIXb6G.js +1 -0
- package/static/app/assets/textarea-DH7ne8VI.js +1 -0
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/static/app/assets/Brain-C9ITUsQ_.js +0 -321
- package/static/app/assets/Capture-C-ppTeud.js +0 -1
- package/static/app/assets/Library-CGQbgWTu.js +0 -1
- package/static/app/assets/System-djmj0n2_.js +0 -1
- package/static/app/assets/index-AF0-4XVv.js +0 -18
- package/static/app/assets/primitives-jbb2qv4Q.js +0 -1
- package/static/app/assets/textarea-CFoo0OxJ.js +0 -1
package/latticeai/core/agent.py
CHANGED
|
@@ -319,28 +319,8 @@ class SingleAgentRuntime:
|
|
|
319
319
|
parse_failures = 0
|
|
320
320
|
|
|
321
321
|
for _ in range(budget):
|
|
322
|
-
corrections_hint = (
|
|
323
|
-
"\n\nCritic corrections from previous attempt:\n"
|
|
324
|
-
+ "\n".join(f"- {c}" for c in ctx.corrections)
|
|
325
|
-
) if ctx.corrections else ""
|
|
326
|
-
|
|
327
322
|
request_workspace = getattr(req, "workspace_id", None)
|
|
328
|
-
|
|
329
|
-
"conversation_id": req.conversation_id,
|
|
330
|
-
"user_email": current_user or None,
|
|
331
|
-
}
|
|
332
|
-
if request_workspace is not None:
|
|
333
|
-
recent_kwargs["workspace_id"] = request_workspace
|
|
334
|
-
recent_conversation = d.recent_chat_context(**recent_kwargs) or "(none)"
|
|
335
|
-
context = (
|
|
336
|
-
f"{d.executor_prompt}\n\n"
|
|
337
|
-
f"[LANGUAGE HINT: {lang_hint}]\n"
|
|
338
|
-
f"Workspace root: {d.agent_root}\n\n"
|
|
339
|
-
f"PLAN:\n{json.dumps(ctx.plan, ensure_ascii=False)}\n\n"
|
|
340
|
-
f"Recent conversation:\n{recent_conversation}\n\n"
|
|
341
|
-
f"User request: {req.message}{corrections_hint}\n\n"
|
|
342
|
-
f"Execution transcript:\n{json.dumps(ctx.transcript, ensure_ascii=False, indent=2)}"
|
|
343
|
-
)
|
|
323
|
+
context = self._executor_context(ctx, req, lang_hint, current_user, request_workspace)
|
|
344
324
|
raw = await d.generate_as(
|
|
345
325
|
model_id,
|
|
346
326
|
message="Execute the next step.",
|
|
@@ -352,33 +332,8 @@ class SingleAgentRuntime:
|
|
|
352
332
|
ctx.trace.repair("execute", repairs=exec_repairs)
|
|
353
333
|
except ValueError as exc:
|
|
354
334
|
parse_failures += 1
|
|
355
|
-
|
|
356
|
-
"state": AgentState.EXECUTING.value, "action": "parse_error",
|
|
357
|
-
"raw": str(raw)[:400], "error": str(exc),
|
|
358
|
-
})
|
|
359
|
-
if parse_failures >= 3:
|
|
360
|
-
ctx.trace.parse_error("execute", error=str(exc), recovered=False)
|
|
335
|
+
if self._note_parse_failure(ctx, raw, exc, parse_failures):
|
|
361
336
|
break
|
|
362
|
-
ctx.trace.parse_error("execute", error=str(exc), recovered=True)
|
|
363
|
-
# Weak models often need one concrete reminder of the wire
|
|
364
|
-
# format; feed it through the corrections channel and retry
|
|
365
|
-
# instead of aborting the whole run on the first slip.
|
|
366
|
-
hint = (
|
|
367
|
-
'Your last reply was not a single JSON action object. Reply with '
|
|
368
|
-
'EXACTLY one JSON object like {"thoughts": "...", "action": '
|
|
369
|
-
'"tool_name", "args": {...}} and nothing else.'
|
|
370
|
-
)
|
|
371
|
-
if parse_failures >= 2:
|
|
372
|
-
# Escalate: name the valid tools so the model stops
|
|
373
|
-
# inventing action names or prose.
|
|
374
|
-
valid = ", ".join(sorted(d.tool_governance.keys()))
|
|
375
|
-
hint = (
|
|
376
|
-
f"{hint} Valid action values are: {valid}, final. "
|
|
377
|
-
'Use {"action": "final", "message": "..."} to finish.'
|
|
378
|
-
)
|
|
379
|
-
if hint not in ctx.corrections:
|
|
380
|
-
ctx.corrections.append(hint)
|
|
381
|
-
ctx.trace.correction("execute", hint=hint)
|
|
382
337
|
continue
|
|
383
338
|
|
|
384
339
|
name = action.get("action")
|
|
@@ -402,14 +357,7 @@ class SingleAgentRuntime:
|
|
|
402
357
|
return
|
|
403
358
|
|
|
404
359
|
# Loop guard
|
|
405
|
-
|
|
406
|
-
last = exec_steps[-1] if exec_steps else None
|
|
407
|
-
if (
|
|
408
|
-
name in d.file_create_actions and last
|
|
409
|
-
and last.get("action") == name
|
|
410
|
-
and (last.get("args") or {}) == args
|
|
411
|
-
and "result" in last
|
|
412
|
-
):
|
|
360
|
+
if self._is_repeated_create(ctx, name, args):
|
|
413
361
|
ctx.transcript.append({
|
|
414
362
|
"state": AgentState.EXECUTING.value, "action": name,
|
|
415
363
|
"error": "LOOP_DETECTED: identical action+args repeated — halted.",
|
|
@@ -428,93 +376,203 @@ class SingleAgentRuntime:
|
|
|
428
376
|
policy = d.policy_for(name, args)
|
|
429
377
|
risk = d.risk_level(policy)
|
|
430
378
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
name, args, policy=dict(policy),
|
|
437
|
-
user_email=current_user, workspace_id=request_workspace,
|
|
438
|
-
)
|
|
439
|
-
if verdict is not None and verdict.get("decision") == "proposed":
|
|
440
|
-
proposal = verdict.get("proposal") or {}
|
|
441
|
-
ctx.trace.tool("execute", name=name, outcome="proposed", risk=risk)
|
|
442
|
-
ctx.transcript.append({
|
|
443
|
-
"state": AgentState.EXECUTING.value, "action": name,
|
|
444
|
-
"thoughts": thoughts, "args": {k: v for k, v in args.items() if k != "content"},
|
|
445
|
-
"risk": risk, "governance": dict(policy),
|
|
446
|
-
"result": {
|
|
447
|
-
"proposed": True,
|
|
448
|
-
"proposal_id": proposal.get("id"),
|
|
449
|
-
"note": "기존 내용을 바꾸는 작업이라 변경 제안으로 저장했습니다. 검토함에서 승인하면 적용됩니다.",
|
|
450
|
-
},
|
|
451
|
-
})
|
|
452
|
-
d.audit(
|
|
453
|
-
"agent_change_proposed", user_email=current_user,
|
|
454
|
-
action=name, proposal_id=proposal.get("id"),
|
|
455
|
-
change_class=(verdict.get("classification") or {}).get("change_class"),
|
|
456
|
-
)
|
|
457
|
-
continue
|
|
458
|
-
governor_allows_additive = (
|
|
459
|
-
verdict is not None and verdict.get("decision") == "allow_additive"
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
if policy["risk"] == "destructive":
|
|
463
|
-
ctx.trace.tool("execute", name=name, outcome="blocked_destructive", risk=risk)
|
|
464
|
-
ctx.transcript.append({
|
|
465
|
-
"state": AgentState.EXECUTING.value, "action": name,
|
|
466
|
-
"thoughts": thoughts, "args": args, "risk": risk,
|
|
467
|
-
"governance": dict(policy),
|
|
468
|
-
"error": f"BLOCKED: destructive action '{name}' not permitted in agent mode.",
|
|
469
|
-
})
|
|
470
|
-
d.audit(
|
|
471
|
-
"agent_blocked", user_email=current_user, source=getattr(req, "source", None) or "agent",
|
|
472
|
-
action=name, reason="destructive", governance=dict(policy),
|
|
473
|
-
)
|
|
379
|
+
proposed, governor_allows_additive = self._governor_review(
|
|
380
|
+
ctx, name, thoughts, args, policy, risk, current_user, request_workspace,
|
|
381
|
+
conversation_id=getattr(req, "conversation_id", None),
|
|
382
|
+
)
|
|
383
|
+
if proposed:
|
|
474
384
|
continue
|
|
475
385
|
|
|
476
|
-
if
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
shell=policy["shell"], network=policy["network"],
|
|
481
|
-
destructive=policy["destructive"], sandbox=policy["sandbox"],
|
|
482
|
-
rollback=policy["rollback"],
|
|
483
|
-
args={k: v for k, v in args.items() if k != "content"},
|
|
484
|
-
)
|
|
485
|
-
ctx.trace.tool("execute", name=name, outcome="blocked_approval", risk=risk)
|
|
486
|
-
ctx.transcript.append({
|
|
487
|
-
"state": AgentState.EXECUTING.value, "action": name,
|
|
488
|
-
"thoughts": thoughts, "args": args, "risk": risk,
|
|
489
|
-
"governance": dict(policy),
|
|
490
|
-
"error": f"BLOCKED: action '{name}' requires explicit approval.",
|
|
491
|
-
})
|
|
386
|
+
if self._blocked_by_gates(
|
|
387
|
+
ctx, req, name, thoughts, args, policy, risk,
|
|
388
|
+
current_user, governor_allows_additive,
|
|
389
|
+
):
|
|
492
390
|
continue
|
|
493
391
|
|
|
494
|
-
|
|
495
|
-
d.check_role(name, current_user)
|
|
496
|
-
# Shared tool lifecycle: pre_tool (may block) → execute → post_tool.
|
|
497
|
-
result = dispatch_tool(
|
|
498
|
-
d.hooks, name, args,
|
|
499
|
-
lambda: d.execute_tool(name, args),
|
|
500
|
-
user_email=current_user, source="agent",
|
|
501
|
-
)
|
|
502
|
-
ctx.trace.tool("execute", name=name, outcome="ok", risk=risk)
|
|
503
|
-
ctx.transcript.append({
|
|
504
|
-
"state": AgentState.EXECUTING.value, "action": name,
|
|
505
|
-
"thoughts": thoughts, "args": args,
|
|
506
|
-
"risk": risk, "governance": dict(policy), "result": result,
|
|
507
|
-
})
|
|
508
|
-
except (ToolError, KeyError, TypeError, PermissionError) as exc:
|
|
509
|
-
ctx.trace.tool("execute", name=name, outcome="error", risk=risk)
|
|
510
|
-
ctx.transcript.append({
|
|
511
|
-
"state": AgentState.EXECUTING.value, "action": name,
|
|
512
|
-
"thoughts": thoughts, "args": args,
|
|
513
|
-
"risk": risk, "governance": dict(policy), "error": str(exc),
|
|
514
|
-
})
|
|
392
|
+
self._dispatch_step(ctx, name, thoughts, args, policy, risk, current_user)
|
|
515
393
|
|
|
516
394
|
ctx.state = AgentState.VERIFYING
|
|
517
395
|
|
|
396
|
+
def _executor_context(
|
|
397
|
+
self, ctx: AgentRunContext, req: Any, lang_hint: str,
|
|
398
|
+
current_user: str, request_workspace: Optional[str],
|
|
399
|
+
) -> str:
|
|
400
|
+
"""Assemble one executor turn's prompt (plan, corrections, recent chat)."""
|
|
401
|
+
d = self.deps
|
|
402
|
+
corrections_hint = (
|
|
403
|
+
"\n\nCritic corrections from previous attempt:\n"
|
|
404
|
+
+ "\n".join(f"- {c}" for c in ctx.corrections)
|
|
405
|
+
) if ctx.corrections else ""
|
|
406
|
+
|
|
407
|
+
recent_kwargs = {
|
|
408
|
+
"conversation_id": req.conversation_id,
|
|
409
|
+
"user_email": current_user or None,
|
|
410
|
+
}
|
|
411
|
+
if request_workspace is not None:
|
|
412
|
+
recent_kwargs["workspace_id"] = request_workspace
|
|
413
|
+
recent_conversation = d.recent_chat_context(**recent_kwargs) or "(none)"
|
|
414
|
+
return (
|
|
415
|
+
f"{d.executor_prompt}\n\n"
|
|
416
|
+
f"[LANGUAGE HINT: {lang_hint}]\n"
|
|
417
|
+
f"Workspace root: {d.agent_root}\n\n"
|
|
418
|
+
f"PLAN:\n{json.dumps(ctx.plan, ensure_ascii=False)}\n\n"
|
|
419
|
+
f"Recent conversation:\n{recent_conversation}\n\n"
|
|
420
|
+
f"User request: {req.message}{corrections_hint}\n\n"
|
|
421
|
+
f"Execution transcript:\n{json.dumps(ctx.transcript, ensure_ascii=False, indent=2)}"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def _note_parse_failure(
|
|
425
|
+
self, ctx: AgentRunContext, raw: Any, exc: ValueError, parse_failures: int,
|
|
426
|
+
) -> bool:
|
|
427
|
+
"""Record one executor parse slip; True when the run should stop retrying."""
|
|
428
|
+
ctx.transcript.append({
|
|
429
|
+
"state": AgentState.EXECUTING.value, "action": "parse_error",
|
|
430
|
+
"raw": str(raw)[:400], "error": str(exc),
|
|
431
|
+
})
|
|
432
|
+
if parse_failures >= 3:
|
|
433
|
+
ctx.trace.parse_error("execute", error=str(exc), recovered=False)
|
|
434
|
+
return True
|
|
435
|
+
ctx.trace.parse_error("execute", error=str(exc), recovered=True)
|
|
436
|
+
# Weak models often need one concrete reminder of the wire
|
|
437
|
+
# format; feed it through the corrections channel and retry
|
|
438
|
+
# instead of aborting the whole run on the first slip.
|
|
439
|
+
hint = (
|
|
440
|
+
'Your last reply was not a single JSON action object. Reply with '
|
|
441
|
+
'EXACTLY one JSON object like {"thoughts": "...", "action": '
|
|
442
|
+
'"tool_name", "args": {...}} and nothing else.'
|
|
443
|
+
)
|
|
444
|
+
if parse_failures >= 2:
|
|
445
|
+
# Escalate: name the valid tools so the model stops
|
|
446
|
+
# inventing action names or prose.
|
|
447
|
+
valid = ", ".join(sorted(self.deps.tool_governance.keys()))
|
|
448
|
+
hint = (
|
|
449
|
+
f"{hint} Valid action values are: {valid}, final. "
|
|
450
|
+
'Use {"action": "final", "message": "..."} to finish.'
|
|
451
|
+
)
|
|
452
|
+
if hint not in ctx.corrections:
|
|
453
|
+
ctx.corrections.append(hint)
|
|
454
|
+
ctx.trace.correction("execute", hint=hint)
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
def _is_repeated_create(self, ctx: AgentRunContext, name: Any, args: dict) -> bool:
|
|
458
|
+
"""Loop guard: the same file-create action+args re-issued right after a result."""
|
|
459
|
+
exec_steps = [s for s in ctx.transcript if s.get("state") == AgentState.EXECUTING.value]
|
|
460
|
+
last = exec_steps[-1] if exec_steps else None
|
|
461
|
+
return bool(
|
|
462
|
+
name in self.deps.file_create_actions and last
|
|
463
|
+
and last.get("action") == name
|
|
464
|
+
and (last.get("args") or {}) == args
|
|
465
|
+
and "result" in last
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def _governor_review(
|
|
469
|
+
self, ctx: AgentRunContext, name: str, thoughts: str, args: dict,
|
|
470
|
+
policy: dict, risk: str, current_user: str, request_workspace: Optional[str],
|
|
471
|
+
conversation_id: Optional[str] = None,
|
|
472
|
+
) -> Tuple[bool, bool]:
|
|
473
|
+
"""Central change-class governance: create-new runs with minimal
|
|
474
|
+
friction, change/delete-existing becomes a review proposal.
|
|
475
|
+
|
|
476
|
+
Returns ``(proposed, governor_allows_additive)``: ``proposed`` means the
|
|
477
|
+
step was staged as a proposal (skip execution); ``allows_additive`` lets
|
|
478
|
+
an additive create pass the classic approval gate.
|
|
479
|
+
"""
|
|
480
|
+
d = self.deps
|
|
481
|
+
if d.change_governor is None:
|
|
482
|
+
return False, False
|
|
483
|
+
verdict = d.change_governor.review(
|
|
484
|
+
name, args, policy=dict(policy),
|
|
485
|
+
user_email=current_user, workspace_id=request_workspace,
|
|
486
|
+
conversation_id=conversation_id,
|
|
487
|
+
)
|
|
488
|
+
if verdict is not None and verdict.get("decision") == "proposed":
|
|
489
|
+
proposal = verdict.get("proposal") or {}
|
|
490
|
+
ctx.trace.tool("execute", name=name, outcome="proposed", risk=risk)
|
|
491
|
+
ctx.transcript.append({
|
|
492
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
493
|
+
"thoughts": thoughts, "args": {k: v for k, v in args.items() if k != "content"},
|
|
494
|
+
"risk": risk, "governance": dict(policy),
|
|
495
|
+
"result": {
|
|
496
|
+
"proposed": True,
|
|
497
|
+
"proposal_id": proposal.get("id"),
|
|
498
|
+
"note": "기존 내용을 바꾸는 작업이라 변경 제안으로 저장했습니다. 검토함에서 승인하면 적용됩니다.",
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
d.audit(
|
|
502
|
+
"agent_change_proposed", user_email=current_user,
|
|
503
|
+
action=name, proposal_id=proposal.get("id"),
|
|
504
|
+
change_class=(verdict.get("classification") or {}).get("change_class"),
|
|
505
|
+
)
|
|
506
|
+
return True, False
|
|
507
|
+
return False, (verdict is not None and verdict.get("decision") == "allow_additive")
|
|
508
|
+
|
|
509
|
+
def _blocked_by_gates(
|
|
510
|
+
self, ctx: AgentRunContext, req: Any, name: str, thoughts: str, args: dict,
|
|
511
|
+
policy: dict, risk: str, current_user: str, governor_allows_additive: bool,
|
|
512
|
+
) -> bool:
|
|
513
|
+
"""Classic destructive / explicit-approval gates; True when the step was blocked."""
|
|
514
|
+
d = self.deps
|
|
515
|
+
if policy["risk"] == "destructive":
|
|
516
|
+
ctx.trace.tool("execute", name=name, outcome="blocked_destructive", risk=risk)
|
|
517
|
+
ctx.transcript.append({
|
|
518
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
519
|
+
"thoughts": thoughts, "args": args, "risk": risk,
|
|
520
|
+
"governance": dict(policy),
|
|
521
|
+
"error": f"BLOCKED: destructive action '{name}' not permitted in agent mode.",
|
|
522
|
+
})
|
|
523
|
+
d.audit(
|
|
524
|
+
"agent_blocked", user_email=current_user, source=getattr(req, "source", None) or "agent",
|
|
525
|
+
action=name, reason="destructive", governance=dict(policy),
|
|
526
|
+
)
|
|
527
|
+
return True
|
|
528
|
+
|
|
529
|
+
if not policy["auto_approve"] and not ctx.approved_by_human and not governor_allows_additive:
|
|
530
|
+
d.audit(
|
|
531
|
+
"agent_exec", user_email=current_user, source=getattr(req, "source", None) or "agent",
|
|
532
|
+
state=AgentState.EXECUTING.value, action=name, risk=risk,
|
|
533
|
+
shell=policy["shell"], network=policy["network"],
|
|
534
|
+
destructive=policy["destructive"], sandbox=policy["sandbox"],
|
|
535
|
+
rollback=policy["rollback"],
|
|
536
|
+
args={k: v for k, v in args.items() if k != "content"},
|
|
537
|
+
)
|
|
538
|
+
ctx.trace.tool("execute", name=name, outcome="blocked_approval", risk=risk)
|
|
539
|
+
ctx.transcript.append({
|
|
540
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
541
|
+
"thoughts": thoughts, "args": args, "risk": risk,
|
|
542
|
+
"governance": dict(policy),
|
|
543
|
+
"error": f"BLOCKED: action '{name}' requires explicit approval.",
|
|
544
|
+
})
|
|
545
|
+
return True
|
|
546
|
+
return False
|
|
547
|
+
|
|
548
|
+
def _dispatch_step(
|
|
549
|
+
self, ctx: AgentRunContext, name: str, thoughts: str, args: dict,
|
|
550
|
+
policy: dict, risk: str, current_user: str,
|
|
551
|
+
) -> None:
|
|
552
|
+
"""Role check + shared tool lifecycle, recorded on the transcript either way."""
|
|
553
|
+
d = self.deps
|
|
554
|
+
try:
|
|
555
|
+
d.check_role(name, current_user)
|
|
556
|
+
# Shared tool lifecycle: pre_tool (may block) → execute → post_tool.
|
|
557
|
+
result = dispatch_tool(
|
|
558
|
+
d.hooks, name, args,
|
|
559
|
+
lambda: d.execute_tool(name, args),
|
|
560
|
+
user_email=current_user, source="agent",
|
|
561
|
+
)
|
|
562
|
+
ctx.trace.tool("execute", name=name, outcome="ok", risk=risk)
|
|
563
|
+
ctx.transcript.append({
|
|
564
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
565
|
+
"thoughts": thoughts, "args": args,
|
|
566
|
+
"risk": risk, "governance": dict(policy), "result": result,
|
|
567
|
+
})
|
|
568
|
+
except (ToolError, KeyError, TypeError, PermissionError) as exc:
|
|
569
|
+
ctx.trace.tool("execute", name=name, outcome="error", risk=risk)
|
|
570
|
+
ctx.transcript.append({
|
|
571
|
+
"state": AgentState.EXECUTING.value, "action": name,
|
|
572
|
+
"thoughts": thoughts, "args": args,
|
|
573
|
+
"risk": risk, "governance": dict(policy), "error": str(exc),
|
|
574
|
+
})
|
|
575
|
+
|
|
518
576
|
# ── VERIFY ───────────────────────────────────────────────────────
|
|
519
577
|
async def verify(
|
|
520
578
|
self, ctx: AgentRunContext, req: Any, lang_hint: str, current_user: str,
|