tamfis-code 0.7.0__tar.gz → 1.3.0__tar.gz
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.
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/PKG-INFO +1 -1
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/pyproject.toml +2 -2
- tamfis_code-1.3.0/setup.py +2 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/__init__.py +1 -1
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/cli.py +156 -7
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/doctor.py +15 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/interactive.py +34 -1
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/live_input.py +56 -39
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/local_chat.py +33 -2
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/mcp.py +40 -7
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/agent.py +2 -2
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/__init__.py +2 -0
- tamfis_code-1.3.0/tamfis_code/orchestrator/approvals.py +64 -0
- tamfis_code-1.3.0/tamfis_code/orchestrator/completion.py +48 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/engine.py +15 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/planner.py +41 -0
- tamfis_code-1.3.0/tamfis_code/orchestrator/repair.py +68 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/providers.py +219 -19
- tamfis_code-1.3.0/tamfis_code/release_verification.py +276 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/render.py +1 -1
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runner.py +26 -1
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runner_local.py +127 -6
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/__init__.py +6 -0
- tamfis_code-1.3.0/tamfis_code/runtime/checkpoint.py +79 -0
- tamfis_code-1.3.0/tamfis_code/runtime/cognitive.py +147 -0
- tamfis_code-1.3.0/tamfis_code/runtime/journal.py +119 -0
- tamfis_code-1.3.0/tamfis_code/runtime/memory.py +210 -0
- tamfis_code-1.3.0/tamfis_code/runtime/repository_index.py +82 -0
- tamfis_code-1.3.0/tamfis_code/runtime/reviewer.py +24 -0
- tamfis_code-1.3.0/tamfis_code/runtime/steering.py +31 -0
- tamfis_code-1.3.0/tamfis_code/runtime/unified.py +301 -0
- tamfis_code-1.3.0/tamfis_code/runtime/workspace_authority.py +132 -0
- tamfis_code-1.3.0/tamfis_code/runtime/worktree.py +109 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/PKG-INFO +1 -1
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/SOURCES.txt +25 -0
- tamfis_code-1.3.0/tests/test_phase1_reliability.py +94 -0
- tamfis_code-1.3.0/tests/test_phase2_unified_runtime.py +80 -0
- tamfis_code-1.3.0/tests/test_phase3_claude_behaviour.py +110 -0
- tamfis_code-1.3.0/tests/test_phase4_release_verification.py +26 -0
- tamfis_code-1.3.0/tests/test_phase5_cognitive_orchestration.py +92 -0
- tamfis_code-1.3.0/tests/test_phase6_workspace_authority.py +65 -0
- tamfis_code-1.3.0/tests/test_phase7_memory.py +65 -0
- tamfis_code-1.3.0/tests/test_phase8_worktree.py +66 -0
- tamfis_code-1.3.0/tests/test_phase9_decision_logic.py +49 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/README.md +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/setup.cfg +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/__main__.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/agent_definitions.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/agents.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/api_client.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/clipboard.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/completion.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/config.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/custom_commands.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/enforcer.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/evidence.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/github_commands.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/hooks.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/indexer.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/instructions.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/local_tools.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/mcp_stdio_server.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/metrics.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/model_registry.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/__init__.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/agent_server.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/automation.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/compat.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/conversation.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/delegation.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/events.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/leases.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/security.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/skills.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/tools.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/workspace.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/context.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/protocols.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/validator.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/project.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/provider_protocols.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/pty.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/references.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/routing.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/budgets.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/controller.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/evidence.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/state.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/safety.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/screenshot.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/state.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/swarm.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/tasks.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/tool_policy.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/workspace.py +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/dependency_links.txt +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/entry_points.txt +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/requires.txt +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/top_level.txt +0 -0
- {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tests/test_runtime_controller.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tamfis-code
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: TamfisGPT Code -- portable subscription-backed terminal coding agent with local tools, PTY, plans, queues, and approvals
|
|
5
5
|
Project-URL: Homepage, https://github.com/tamfitronics/tamfis-code
|
|
6
6
|
Project-URL: Repository, https://github.com/tamfitronics/tamfis-code
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tamfis-code"
|
|
3
|
-
version = "
|
|
3
|
+
version = "1.3.0"
|
|
4
4
|
description = "TamfisGPT Code -- portable subscription-backed terminal coding agent with local tools, PTY, plans, queues, and approvals"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -10,7 +10,7 @@ dependencies = [
|
|
|
10
10
|
"rich>=13.0",
|
|
11
11
|
"prompt_toolkit>=3.0",
|
|
12
12
|
# Provider calls (providers.py) go through the openai SDK's client against
|
|
13
|
-
# each
|
|
13
|
+
# each provider's OpenAI-compatible endpoint (Ollama Cloud, NVIDIA NIM, HF, OpenRouter)
|
|
14
14
|
# -- this was imported but never declared, only working because a
|
|
15
15
|
# copy happened to already be installed system-wide.
|
|
16
16
|
"openai>=1.30",
|
|
@@ -36,6 +36,8 @@ from .api_client import (
|
|
|
36
36
|
)
|
|
37
37
|
from .config import APPROVAL_MODES, CONFIG_DIR, Config, Credentials, load_config
|
|
38
38
|
from .doctor import run_doctor
|
|
39
|
+
from .runtime.memory import MemoryRecord, MemoryType, get_memory_store
|
|
40
|
+
from .runtime.worktree import WorktreeError, create_worktree, list_worktrees, remove_worktree
|
|
39
41
|
from .render import StreamRenderer, print_banner, print_error, print_recent_thread, print_resume_plan_status, print_unified_diff
|
|
40
42
|
from .runner import (
|
|
41
43
|
ACTIVE_TASK_STATUSES,
|
|
@@ -53,7 +55,7 @@ from .local_chat import _PROVIDER_ALIASES
|
|
|
53
55
|
# validation and then failed with "Unknown local provider" inside
|
|
54
56
|
# resolve_provider_type every time.
|
|
55
57
|
_PROVIDER_CHOICES = sorted(_PROVIDER_ALIASES.keys())
|
|
56
|
-
_PROVIDER_HELP = "tamfis/tamfisgpt (subscription API),
|
|
58
|
+
_PROVIDER_HELP = "tamfis/tamfisgpt (subscription API), ollama_cloud, nvidia, hf, openrouter, or auto (default)."
|
|
57
59
|
|
|
58
60
|
EXIT_OK = 0
|
|
59
61
|
EXIT_TASK_FAILED = 1
|
|
@@ -100,7 +102,7 @@ def async_command(fn):
|
|
|
100
102
|
@click.option("--approval", "approval_policy", type=click.Choice(APPROVAL_MODES), default=None, help="Override the configured approval policy for this invocation. Note: 'never' means deny everything outright -- the opposite of 'auto'/'full-auto' (which mean never PROMPT, i.e. auto-approve). It is not a synonym for auto-approve.")
|
|
101
103
|
@click.option("--api-base", "api_base", default=None, help="Override the configured Remote API base URL.")
|
|
102
104
|
@click.option("--cwd", "cwd_override", type=click.Path(exists=True, file_okay=False), default=None, help="Treat this directory as the workspace instead of the current directory.")
|
|
103
|
-
@click.option("--provider", default="auto", help="
|
|
105
|
+
@click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default) -- which provider the bare (no-subcommand) interactive REPL calls directly.")
|
|
104
106
|
@click.option("--model", default=None, help="Provider-specific model id for the bare interactive REPL; defaults to that provider's default model.")
|
|
105
107
|
@click.option("--remote", is_flag=True, default=False, help="Use the legacy TamfisGPT Remote Workspace backend for the bare interactive REPL instead of calling a provider directly.")
|
|
106
108
|
@click.version_option(__version__, prog_name="tamfis-code")
|
|
@@ -343,6 +345,125 @@ def workspace_remove(ctx: click.Context, path: Path):
|
|
|
343
345
|
console.print(f"[green]Workspace removed:[/green] {target}")
|
|
344
346
|
|
|
345
347
|
|
|
348
|
+
@cli.group(name="memory")
|
|
349
|
+
def memory_group():
|
|
350
|
+
"""Manage durable, cross-session memory records."""
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@memory_group.command(name="list")
|
|
354
|
+
@click.option("--type", "memory_type", type=click.Choice([t.value for t in MemoryType]), default=None)
|
|
355
|
+
def memory_list(memory_type: Optional[str]):
|
|
356
|
+
console = Console()
|
|
357
|
+
store = get_memory_store()
|
|
358
|
+
records = store.list(MemoryType(memory_type) if memory_type else None)
|
|
359
|
+
if not records:
|
|
360
|
+
console.print("[dim]No memory records.[/dim]")
|
|
361
|
+
return
|
|
362
|
+
table = Table(show_header=True, header_style="bold")
|
|
363
|
+
table.add_column("name")
|
|
364
|
+
table.add_column("type")
|
|
365
|
+
table.add_column("description")
|
|
366
|
+
for record in records:
|
|
367
|
+
table.add_row(record.name, record.type.value, record.description)
|
|
368
|
+
console.print(table)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@memory_group.command(name="show")
|
|
372
|
+
@click.argument("name")
|
|
373
|
+
def memory_show(name: str):
|
|
374
|
+
console = Console()
|
|
375
|
+
record = get_memory_store().load(name)
|
|
376
|
+
if record is None:
|
|
377
|
+
print_error(console, f"No memory record named {name!r}.")
|
|
378
|
+
raise SystemExit(EXIT_TASK_FAILED)
|
|
379
|
+
console.print(f"[bold]{record.name}[/bold] ({record.type.value})")
|
|
380
|
+
console.print(record.description)
|
|
381
|
+
console.print()
|
|
382
|
+
console.print(record.content)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@memory_group.command(name="add")
|
|
386
|
+
@click.argument("name")
|
|
387
|
+
@click.option("--type", "memory_type", type=click.Choice([t.value for t in MemoryType]), required=True)
|
|
388
|
+
@click.option("--description", required=True)
|
|
389
|
+
@click.option("--content", required=True)
|
|
390
|
+
def memory_add(name: str, memory_type: str, description: str, content: str):
|
|
391
|
+
console = Console()
|
|
392
|
+
record = get_memory_store().save(
|
|
393
|
+
MemoryRecord(name=name, type=MemoryType(memory_type), description=description, content=content)
|
|
394
|
+
)
|
|
395
|
+
console.print(f"[green]Saved memory record:[/green] {record.name}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@memory_group.command(name="forget")
|
|
399
|
+
@click.argument("name")
|
|
400
|
+
def memory_forget(name: str):
|
|
401
|
+
console = Console()
|
|
402
|
+
if get_memory_store().delete(name):
|
|
403
|
+
console.print(f"[green]Forgot memory record:[/green] {name}")
|
|
404
|
+
else:
|
|
405
|
+
print_error(console, f"No memory record named {name!r}.")
|
|
406
|
+
raise SystemExit(EXIT_TASK_FAILED)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@cli.group(name="worktree")
|
|
410
|
+
def worktree_group():
|
|
411
|
+
"""Manage isolated git worktrees for background/parallel execution."""
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@worktree_group.command(name="list")
|
|
415
|
+
@click.pass_context
|
|
416
|
+
def worktree_list_cmd(ctx: click.Context):
|
|
417
|
+
console = Console(no_color=not ctx.obj["config"].colour)
|
|
418
|
+
try:
|
|
419
|
+
handles = list_worktrees(ctx.obj["workspace_root"])
|
|
420
|
+
except WorktreeError as exc:
|
|
421
|
+
print_error(console, str(exc))
|
|
422
|
+
raise SystemExit(EXIT_TASK_FAILED)
|
|
423
|
+
if not handles:
|
|
424
|
+
console.print("[dim]No worktrees.[/dim]")
|
|
425
|
+
return
|
|
426
|
+
table = Table(show_header=True, header_style="bold")
|
|
427
|
+
table.add_column("branch")
|
|
428
|
+
table.add_column("path")
|
|
429
|
+
for handle in handles:
|
|
430
|
+
table.add_row(handle.branch, str(handle.path))
|
|
431
|
+
console.print(table)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@worktree_group.command(name="create")
|
|
435
|
+
@click.argument("branch")
|
|
436
|
+
@click.option("--base", default="HEAD", help="Ref the new worktree branch is created from.")
|
|
437
|
+
@click.pass_context
|
|
438
|
+
def worktree_create_cmd(ctx: click.Context, branch: str, base: str):
|
|
439
|
+
console = Console(no_color=not ctx.obj["config"].colour)
|
|
440
|
+
try:
|
|
441
|
+
handle = create_worktree(ctx.obj["workspace_root"], branch=branch, base=base)
|
|
442
|
+
except WorktreeError as exc:
|
|
443
|
+
print_error(console, str(exc))
|
|
444
|
+
raise SystemExit(EXIT_TASK_FAILED)
|
|
445
|
+
console.print(f"[green]Worktree created:[/green] {handle.path} (branch {handle.branch})")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@worktree_group.command(name="remove")
|
|
449
|
+
@click.argument("branch")
|
|
450
|
+
@click.option("--force", is_flag=True, default=False, help="Remove even if the worktree has uncommitted changes.")
|
|
451
|
+
@click.pass_context
|
|
452
|
+
def worktree_remove_cmd(ctx: click.Context, branch: str, force: bool):
|
|
453
|
+
console = Console(no_color=not ctx.obj["config"].colour)
|
|
454
|
+
try:
|
|
455
|
+
handles = {h.branch: h for h in list_worktrees(ctx.obj["workspace_root"])}
|
|
456
|
+
handle = handles.get(branch)
|
|
457
|
+
if handle is None:
|
|
458
|
+
print_error(console, f"No worktree found for branch {branch!r}.")
|
|
459
|
+
raise SystemExit(EXIT_TASK_FAILED)
|
|
460
|
+
remove_worktree(handle, force=force)
|
|
461
|
+
except WorktreeError as exc:
|
|
462
|
+
print_error(console, str(exc))
|
|
463
|
+
raise SystemExit(EXIT_TASK_FAILED)
|
|
464
|
+
console.print(f"[green]Worktree removed:[/green] {branch}")
|
|
465
|
+
|
|
466
|
+
|
|
346
467
|
@cli.command(name="cwd")
|
|
347
468
|
@click.argument("path", required=False, type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
348
469
|
@click.option("--remote", is_flag=True, default=False, help="Update the working directory on the legacy TamfisGPT Remote Workspace backend instead of the local session.")
|
|
@@ -422,7 +543,7 @@ async def init(ctx: click.Context, remote: bool):
|
|
|
422
543
|
|
|
423
544
|
|
|
424
545
|
@cli.command()
|
|
425
|
-
@click.option("--provider", default="auto", help="
|
|
546
|
+
@click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
|
|
426
547
|
@click.option("--remote", is_flag=True, default=False, help="Check the legacy TamfisGPT Remote Workspace backend instead of local provider connectivity.")
|
|
427
548
|
@click.pass_context
|
|
428
549
|
@async_command
|
|
@@ -1359,7 +1480,7 @@ async def run(ctx: click.Context, command: str, background: bool, remote: bool):
|
|
|
1359
1480
|
|
|
1360
1481
|
@cli.command()
|
|
1361
1482
|
@click.argument("session_id", type=int, required=False, default=None)
|
|
1362
|
-
@click.option("--provider", default="auto", help="
|
|
1483
|
+
@click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
|
|
1363
1484
|
@click.option("--model", default=None, help="Provider-specific model id; defaults to that provider's default model.")
|
|
1364
1485
|
@click.option("--remote", is_flag=True, default=False, help="Resume a session on the legacy TamfisGPT Remote Workspace backend instead of a local one.")
|
|
1365
1486
|
@click.pass_context
|
|
@@ -1426,7 +1547,7 @@ async def resume(ctx: click.Context, session_id: Optional[int], provider: str, m
|
|
|
1426
1547
|
|
|
1427
1548
|
@cli.command()
|
|
1428
1549
|
@click.argument("task_id", required=False, default=None)
|
|
1429
|
-
@click.option("--provider", default="auto", help="
|
|
1550
|
+
@click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
|
|
1430
1551
|
@click.option("--model", default=None, help="Provider-specific model id; defaults to that provider's default model.")
|
|
1431
1552
|
@click.option("--remote", is_flag=True, default=False, help="Retry a task on the legacy TamfisGPT Remote Workspace backend instead of resending locally.")
|
|
1432
1553
|
@click.pass_context
|
|
@@ -1948,7 +2069,7 @@ def completion_cmd(shell: str):
|
|
|
1948
2069
|
@click.option('--task', '-t', 'tasks', multiple=True, help='Task description (repeatable for delegate)')
|
|
1949
2070
|
@click.option('--file', '-f', help='File to operate on')
|
|
1950
2071
|
@click.option('--max-concurrency', default=1, show_default=True, help='Max concurrent delegated sub-tasks')
|
|
1951
|
-
@click.option('--provider', default="auto", help="
|
|
2072
|
+
@click.option('--provider', default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
|
|
1952
2073
|
@click.option('--model', default=None, help="Provider-specific model id; defaults to that provider's default model.")
|
|
1953
2074
|
@click.pass_context
|
|
1954
2075
|
def agent_cmd(ctx: click.Context, action: str, tasks: tuple[str, ...], file: str, max_concurrency: int, provider: str, model: Optional[str]):
|
|
@@ -2189,7 +2310,7 @@ def providers_command(ctx: click.Context):
|
|
|
2189
2310
|
|
|
2190
2311
|
@cli.command('local')
|
|
2191
2312
|
@click.argument('objective', required=False)
|
|
2192
|
-
@click.option('--provider', default="auto", help="
|
|
2313
|
+
@click.option('--provider', default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
|
|
2193
2314
|
@click.option('--model', default=None, help="Provider-specific model id; defaults to that provider's default model.")
|
|
2194
2315
|
@click.option('--no-tools', 'no_tools', is_flag=True, default=False, help="Disable read-only repo tools (read_file/list_directory/search_code/get_git_info) for this turn.")
|
|
2195
2316
|
@click.option('--agent', 'full_agent', is_flag=True, default=False, help="Full read/write/execute tool access (write_file/edit_file/execute_command) via the local risk/approval/mutation-ledger layer, instead of read-only Q&A. Standalone -- no TamfisGPT backend involved.")
|
|
@@ -2382,6 +2503,34 @@ from .enforcer import TestEnforcer, run_enforcer, add_enforcer_command
|
|
|
2382
2503
|
# TestEnforcer -- using those flags raised AttributeError at runtime.
|
|
2383
2504
|
add_enforcer_command(cli)
|
|
2384
2505
|
|
|
2506
|
+
@cli.command("verify-release")
|
|
2507
|
+
@click.option("--artifact", "artifacts", multiple=True, type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Built wheel, source archive, checksum, or manifest to verify.")
|
|
2508
|
+
@click.option("--output-dir", type=click.Path(file_okay=False, path_type=Path), default=None, help="Directory for JSON and Markdown verification reports.")
|
|
2509
|
+
@click.pass_context
|
|
2510
|
+
def verify_release_command(ctx: click.Context, artifacts: tuple[Path, ...], output_dir: Optional[Path]):
|
|
2511
|
+
"""Run the Phase 4 release gate without contacting any AI provider."""
|
|
2512
|
+
from .release_verification import run_release_verification
|
|
2513
|
+
|
|
2514
|
+
root: Path = ctx.obj["workspace_root"]
|
|
2515
|
+
report = run_release_verification(root, artifacts)
|
|
2516
|
+
console = Console(no_color=not ctx.obj["config"].colour)
|
|
2517
|
+
table = Table(show_header=True, header_style="bold")
|
|
2518
|
+
table.add_column("Category")
|
|
2519
|
+
table.add_column("Check")
|
|
2520
|
+
table.add_column("Result")
|
|
2521
|
+
table.add_column("Detail")
|
|
2522
|
+
for check in report.checks:
|
|
2523
|
+
table.add_row(check.category, check.name, "PASS" if check.passed else "FAIL", check.detail)
|
|
2524
|
+
console.print(table)
|
|
2525
|
+
|
|
2526
|
+
destination = output_dir or (root / "dist")
|
|
2527
|
+
json_path = report.write_json(destination / f"tamfis_code-{report.version}-verification-report.json")
|
|
2528
|
+
md_path = report.write_markdown(destination / f"tamfis_code-{report.version}-verification-report.md")
|
|
2529
|
+
console.print(f"Verification reports: {json_path} {md_path}")
|
|
2530
|
+
if not report.passed:
|
|
2531
|
+
raise SystemExit(EXIT_TASK_FAILED)
|
|
2532
|
+
|
|
2533
|
+
|
|
2385
2534
|
# Familiar GitHub CLI command surface, delegated to the installed `gh` binary.
|
|
2386
2535
|
from .github_commands import register_github_commands
|
|
2387
2536
|
register_github_commands(cli)
|
|
@@ -21,6 +21,7 @@ from .api_client import AuthRequiredError, RemoteAPIClient, RemoteAPIError
|
|
|
21
21
|
from .config import Config, load_credentials
|
|
22
22
|
from .providers import get_provider_status
|
|
23
23
|
from .workspace import resolve_local_workspace
|
|
24
|
+
from .runtime.journal import JOURNAL_PATH, recent_failures
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@dataclass
|
|
@@ -190,6 +191,20 @@ def _diagnose_local_session(workspace_root: Path) -> list[CheckResult]:
|
|
|
190
191
|
summary = ", ".join(f"{count} {name}" for name, count in sorted(by_status.items())) or "no steps"
|
|
191
192
|
results.append(CheckResult("Active plan step progress", "PASS", summary))
|
|
192
193
|
|
|
194
|
+
failures = recent_failures(5)
|
|
195
|
+
if failures:
|
|
196
|
+
latest = failures[0]
|
|
197
|
+
detail = (
|
|
198
|
+
f"{len(failures)} recent failed/cancelled execution(s); latest "
|
|
199
|
+
f"{latest.get('mode', 'unknown')} at {latest.get('timestamp', 'unknown')}: "
|
|
200
|
+
f"{latest.get('error') or latest.get('status') or 'unknown failure'}"
|
|
201
|
+
)
|
|
202
|
+
results.append(CheckResult("Unified runtime recent failures", "WARNING", detail))
|
|
203
|
+
elif JOURNAL_PATH.exists():
|
|
204
|
+
results.append(CheckResult("Unified runtime recent failures", "PASS", "none in the recent execution journal"))
|
|
205
|
+
else:
|
|
206
|
+
results.append(CheckResult("Unified runtime execution journal", "WARNING", "no unified runtime executions recorded yet"))
|
|
207
|
+
|
|
193
208
|
return results
|
|
194
209
|
|
|
195
210
|
|
|
@@ -107,7 +107,7 @@ async def _run_cancellable_local_turn(
|
|
|
107
107
|
error="Task cancelled by user.",
|
|
108
108
|
)
|
|
109
109
|
finally:
|
|
110
|
-
live_input.stop()
|
|
110
|
+
await live_input.stop()
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
HELP_TEXT = """\
|
|
@@ -589,8 +589,41 @@ async def run_interactive(
|
|
|
589
589
|
break
|
|
590
590
|
except EOFError:
|
|
591
591
|
break
|
|
592
|
+
resolved_paste_count = 0
|
|
593
|
+
resolved_paste_chars = 0
|
|
594
|
+
resolved_paste_lines = 0
|
|
595
|
+
|
|
592
596
|
for placeholder, real_text in pending_pastes.items():
|
|
597
|
+
if placeholder not in text:
|
|
598
|
+
continue
|
|
599
|
+
|
|
593
600
|
text = text.replace(placeholder, real_text)
|
|
601
|
+
resolved_paste_count += 1
|
|
602
|
+
resolved_paste_chars += len(real_text)
|
|
603
|
+
resolved_paste_lines += real_text.count("\n") + (
|
|
604
|
+
0 if real_text.endswith("\n") else 1
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
unresolved_pastes = re.findall(
|
|
608
|
+
r"\[Pasted text #\d+ \+\d+ lines\]",
|
|
609
|
+
text,
|
|
610
|
+
)
|
|
611
|
+
if unresolved_pastes:
|
|
612
|
+
print_error(
|
|
613
|
+
console,
|
|
614
|
+
"Pasted-text resolution failed. The underlying paste was "
|
|
615
|
+
"not available, so no task was submitted. Unresolved: "
|
|
616
|
+
+ ", ".join(unresolved_pastes),
|
|
617
|
+
)
|
|
618
|
+
continue
|
|
619
|
+
|
|
620
|
+
if resolved_paste_count:
|
|
621
|
+
console.print(
|
|
622
|
+
"[dim]diagnostics: Resolved "
|
|
623
|
+
f"{resolved_paste_count} pasted-text block(s), "
|
|
624
|
+
f"{resolved_paste_lines:,} line(s), "
|
|
625
|
+
f"{resolved_paste_chars:,} character(s).[/dim]"
|
|
626
|
+
)
|
|
594
627
|
|
|
595
628
|
text = text.strip()
|
|
596
629
|
if not text:
|
|
@@ -61,21 +61,29 @@ class LiveInputListener:
|
|
|
61
61
|
self._active = True
|
|
62
62
|
self._schedule_prompt()
|
|
63
63
|
|
|
64
|
-
def stop(self) -> None:
|
|
64
|
+
async def stop(self) -> None:
|
|
65
|
+
"""Stop input ownership and wait until prompt-toolkit releases stdin."""
|
|
65
66
|
self._active = False
|
|
66
|
-
self.
|
|
67
|
+
await self._shutdown_prompt()
|
|
67
68
|
if self.renderer.live_input_listener is self:
|
|
68
69
|
self.renderer.live_input_listener = None
|
|
69
|
-
|
|
70
|
+
self.renderer.resume_live()
|
|
70
71
|
|
|
71
72
|
def pause(self) -> None:
|
|
73
|
+
"""Synchronously request prompt shutdown before another UI reads stdin."""
|
|
72
74
|
self._paused = True
|
|
75
|
+
self._request_prompt_exit()
|
|
73
76
|
self._cancel_prompt()
|
|
74
77
|
|
|
75
78
|
def resume(self) -> None:
|
|
76
79
|
self._paused = False
|
|
77
|
-
if self._active:
|
|
78
|
-
|
|
80
|
+
if not self._active:
|
|
81
|
+
return
|
|
82
|
+
task = self._input_task
|
|
83
|
+
if task is not None and not task.done():
|
|
84
|
+
task.add_done_callback(lambda _task: self._schedule_prompt() if self._active and not self._paused else None)
|
|
85
|
+
return
|
|
86
|
+
self._schedule_prompt()
|
|
79
87
|
|
|
80
88
|
def _dispatch(self) -> None:
|
|
81
89
|
"""Compatibility hook for older embedders; start() no longer uses it."""
|
|
@@ -102,11 +110,28 @@ class LiveInputListener:
|
|
|
102
110
|
if self._input_task is None or self._input_task.done():
|
|
103
111
|
self._input_task = asyncio.create_task(self._input_loop())
|
|
104
112
|
|
|
113
|
+
def _request_prompt_exit(self) -> None:
|
|
114
|
+
session = self._prompt_session
|
|
115
|
+
app = getattr(session, "app", None)
|
|
116
|
+
if app is not None and not getattr(app, "is_done", False):
|
|
117
|
+
with contextlib.suppress(Exception):
|
|
118
|
+
app.exit(result="")
|
|
119
|
+
|
|
105
120
|
def _cancel_prompt(self) -> None:
|
|
121
|
+
task = self._input_task
|
|
122
|
+
if task is not None and not task.done():
|
|
123
|
+
task.cancel()
|
|
124
|
+
|
|
125
|
+
async def _shutdown_prompt(self) -> None:
|
|
126
|
+
self._request_prompt_exit()
|
|
106
127
|
task = self._input_task
|
|
107
128
|
self._input_task = None
|
|
108
129
|
if task is not None and not task.done():
|
|
109
130
|
task.cancel()
|
|
131
|
+
if task is not None:
|
|
132
|
+
with contextlib.suppress(asyncio.CancelledError, Exception):
|
|
133
|
+
await task
|
|
134
|
+
self._prompt_session = None
|
|
110
135
|
|
|
111
136
|
def invalidate(self) -> None:
|
|
112
137
|
"""Refresh the prompt footer after a streamed phase/status update."""
|
|
@@ -157,40 +182,32 @@ class LiveInputListener:
|
|
|
157
182
|
|
|
158
183
|
session = PromptSession(key_bindings=bindings)
|
|
159
184
|
self._prompt_session = session
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# even when the listener was deactivated during that prompt.
|
|
187
|
-
# Queue it first, then let the loop terminate cleanly.
|
|
188
|
-
self._enqueue(text)
|
|
189
|
-
|
|
190
|
-
if not self._active:
|
|
191
|
-
break
|
|
192
|
-
|
|
193
|
-
self._prompt_session = None
|
|
185
|
+
try:
|
|
186
|
+
while self._active and not self._paused:
|
|
187
|
+
try:
|
|
188
|
+
with patch_stdout(raw=True):
|
|
189
|
+
text = await session.prompt_async(
|
|
190
|
+
"message> ", bottom_toolbar=self._bottom_toolbar,
|
|
191
|
+
)
|
|
192
|
+
except asyncio.CancelledError:
|
|
193
|
+
raise
|
|
194
|
+
except KeyboardInterrupt:
|
|
195
|
+
self._request_interrupt("cancel")
|
|
196
|
+
return
|
|
197
|
+
except EOFError:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if self._paused:
|
|
201
|
+
break
|
|
202
|
+
self._enqueue(text)
|
|
203
|
+
if not self._active:
|
|
204
|
+
break
|
|
205
|
+
finally:
|
|
206
|
+
if self._prompt_session is session:
|
|
207
|
+
self._prompt_session = None
|
|
208
|
+
current = asyncio.current_task()
|
|
209
|
+
if self._input_task is current:
|
|
210
|
+
self._input_task = None
|
|
194
211
|
|
|
195
212
|
@property
|
|
196
213
|
def interrupt_classification(self) -> Optional[str]:
|
|
@@ -38,7 +38,7 @@ def resolve_provider_type(name: Optional[str]) -> ProviderType:
|
|
|
38
38
|
return _PROVIDER_ALIASES[key]
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
async def
|
|
41
|
+
async def _run_local_turn_impl(
|
|
42
42
|
manager: ProviderManager,
|
|
43
43
|
provider: ProviderType,
|
|
44
44
|
messages: List[Dict[str, Any]],
|
|
@@ -108,7 +108,7 @@ async def run_local_turn(
|
|
|
108
108
|
return "(Stopped after exhausting the local tool-call round limit -- try a narrower question.)"
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
async def
|
|
111
|
+
async def _stream_local_turn_impl(
|
|
112
112
|
manager: ProviderManager,
|
|
113
113
|
provider: ProviderType,
|
|
114
114
|
messages: List[Dict[str, Any]],
|
|
@@ -119,3 +119,34 @@ async def stream_local_turn(
|
|
|
119
119
|
common single-shot/no-tools case still gets real token streaming)."""
|
|
120
120
|
async for chunk in manager.chat_completion(provider, messages, model=model, stream=True):
|
|
121
121
|
yield chunk
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def run_local_turn(
|
|
125
|
+
manager: ProviderManager,
|
|
126
|
+
provider: ProviderType,
|
|
127
|
+
messages: List[Dict[str, Any]],
|
|
128
|
+
model: Optional[str],
|
|
129
|
+
console: Console,
|
|
130
|
+
*,
|
|
131
|
+
use_tools: bool = True,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Compatibility adapter routed through the unified runtime."""
|
|
134
|
+
from .runtime.unified import get_unified_runtime
|
|
135
|
+
return await get_unified_runtime().execute_local_chat(
|
|
136
|
+
manager=manager, provider=provider, messages=messages, model=model,
|
|
137
|
+
console=console, use_tools=use_tools,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def stream_local_turn(
|
|
142
|
+
manager: ProviderManager,
|
|
143
|
+
provider: ProviderType,
|
|
144
|
+
messages: List[Dict[str, Any]],
|
|
145
|
+
model: Optional[str],
|
|
146
|
+
) -> AsyncIterator[str]:
|
|
147
|
+
"""Compatibility streaming adapter routed through the unified runtime."""
|
|
148
|
+
from .runtime.unified import get_unified_runtime
|
|
149
|
+
async for chunk in get_unified_runtime().stream_local_chat(
|
|
150
|
+
manager=manager, provider=provider, messages=messages, model=model,
|
|
151
|
+
):
|
|
152
|
+
yield chunk
|
|
@@ -9,6 +9,8 @@ import re
|
|
|
9
9
|
import subprocess
|
|
10
10
|
import fnmatch
|
|
11
11
|
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
12
14
|
import sys
|
|
13
15
|
import shutil
|
|
14
16
|
import tarfile
|
|
@@ -680,13 +682,31 @@ class MCPServer:
|
|
|
680
682
|
)
|
|
681
683
|
return resolved
|
|
682
684
|
|
|
683
|
-
|
|
685
|
+
def _atomic_write_text(self, target: Path, content: str) -> None:
|
|
686
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
687
|
+
fd, temp_name = tempfile.mkstemp(prefix=f".{target.name}.", suffix=".tmp", dir=str(target.parent))
|
|
688
|
+
try:
|
|
689
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
|
|
690
|
+
handle.write(content)
|
|
691
|
+
handle.flush()
|
|
692
|
+
os.fsync(handle.fileno())
|
|
693
|
+
os.replace(temp_name, target)
|
|
694
|
+
except BaseException:
|
|
695
|
+
with contextlib.suppress(OSError):
|
|
696
|
+
os.unlink(temp_name)
|
|
697
|
+
raise
|
|
698
|
+
|
|
699
|
+
async def _write_file(self, path: str, content: str | None = None, **aliases: Any) -> str:
|
|
700
|
+
content = content if content is not None else aliases.pop("text", None)
|
|
701
|
+
content = content if content is not None else aliases.pop("new_content", None)
|
|
702
|
+
content = content if content is not None else aliases.pop("file_content", None)
|
|
703
|
+
if content is None:
|
|
704
|
+
return "❌ Error: write_file requires content"
|
|
684
705
|
p = self._resolve_in_workspace(path)
|
|
685
706
|
original_content = p.read_text(encoding="utf-8", errors="ignore") if p.is_file() else None
|
|
686
|
-
|
|
687
|
-
p.
|
|
688
|
-
|
|
689
|
-
return f"❌ Failed to write to '{path}'"
|
|
707
|
+
self._atomic_write_text(p, content)
|
|
708
|
+
if p.read_text(encoding="utf-8", errors="strict") != content:
|
|
709
|
+
return f"❌ Failed to verify write to '{path}'"
|
|
690
710
|
if self.session_id is not None:
|
|
691
711
|
from .safety import record_mutation
|
|
692
712
|
record_mutation(
|
|
@@ -696,7 +716,18 @@ class MCPServer:
|
|
|
696
716
|
)
|
|
697
717
|
return f"✅ Successfully wrote {len(content)} bytes to '{path}'"
|
|
698
718
|
|
|
699
|
-
async def _edit_file(
|
|
719
|
+
async def _edit_file(
|
|
720
|
+
self, path: str, old_string: str | None = None, new_string: str | None = None, **aliases: Any
|
|
721
|
+
) -> str:
|
|
722
|
+
old_string = old_string if old_string is not None else aliases.pop("old_text", None)
|
|
723
|
+
new_string = new_string if new_string is not None else aliases.pop("new_text", None)
|
|
724
|
+
new_string = new_string if new_string is not None else aliases.pop("replacement", None)
|
|
725
|
+
full_content = aliases.pop("content", None)
|
|
726
|
+
full_content = full_content if full_content is not None else aliases.pop("new_content", None)
|
|
727
|
+
if full_content is not None and old_string is None:
|
|
728
|
+
return await self._write_file(path, content=full_content)
|
|
729
|
+
if old_string is None or new_string is None:
|
|
730
|
+
return "❌ Error: edit_file requires old_string and new_string, or content for full replacement"
|
|
700
731
|
p = self._resolve_in_workspace(path)
|
|
701
732
|
if not p.is_file():
|
|
702
733
|
return f"❌ Error: File '{path}' not found"
|
|
@@ -710,7 +741,9 @@ class MCPServer:
|
|
|
710
741
|
"Include more surrounding context to disambiguate."
|
|
711
742
|
)
|
|
712
743
|
new_content = original_content.replace(old_string, new_string, 1)
|
|
713
|
-
|
|
744
|
+
self._atomic_write_text(p, new_content)
|
|
745
|
+
if p.read_text(encoding="utf-8", errors="strict") != new_content:
|
|
746
|
+
return f"❌ Failed to verify edit to '{path}'"
|
|
714
747
|
if self.session_id is not None:
|
|
715
748
|
from .safety import record_mutation
|
|
716
749
|
record_mutation(
|
|
@@ -61,7 +61,7 @@ class TamfisAgent:
|
|
|
61
61
|
) -> AgentRunResult:
|
|
62
62
|
from rich.console import Console
|
|
63
63
|
from tamfis_code.providers import ProviderManager, ProviderType
|
|
64
|
-
from tamfis_code.
|
|
64
|
+
from tamfis_code.runtime.unified import get_unified_runtime
|
|
65
65
|
|
|
66
66
|
if self.conversation.state == ConversationState.RUNNING:
|
|
67
67
|
raise RuntimeError("conversation is already running")
|
|
@@ -74,7 +74,7 @@ class TamfisAgent:
|
|
|
74
74
|
except ValueError as exc:
|
|
75
75
|
raise ValueError(f"unsupported provider: {provider}") from exc
|
|
76
76
|
try:
|
|
77
|
-
outcome = await
|
|
77
|
+
outcome = await get_unified_runtime().execute_local(
|
|
78
78
|
manager,
|
|
79
79
|
selected,
|
|
80
80
|
model,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Tamfis-Code orchestration runtime."""
|
|
2
|
+
from .approvals import ApprovalAction, ApprovalBatch, describe_batch
|
|
2
3
|
from .engine import AgentOrchestrator, OrchestrationRun
|
|
3
4
|
from .planner import ExecutionPlan, build_reasoning_plan_prompt, parse_reasoning_plan, should_plan
|
|
4
5
|
from .protocols import AgentPhase, CanonicalEvent, EventType, ToolEnvelope
|
|
@@ -6,4 +7,5 @@ from .protocols import AgentPhase, CanonicalEvent, EventType, ToolEnvelope
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"AgentOrchestrator", "OrchestrationRun", "AgentPhase", "CanonicalEvent", "EventType", "ToolEnvelope",
|
|
8
9
|
"ExecutionPlan", "build_reasoning_plan_prompt", "parse_reasoning_plan", "should_plan",
|
|
10
|
+
"ApprovalAction", "ApprovalBatch", "describe_batch",
|
|
9
11
|
]
|