behalf 0.1.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.
- behalf/__init__.py +48 -0
- behalf/console.py +52 -0
- behalf/core.py +133 -0
- behalf/runner/__init__.py +26 -0
- behalf/runner/adk.py +176 -0
- behalf/runner/sdk.py +116 -0
- behalf/runner/strands.py +179 -0
- behalf-0.1.0.dist-info/METADATA +65 -0
- behalf-0.1.0.dist-info/RECORD +13 -0
- behalf-0.1.0.dist-info/WHEEL +5 -0
- behalf-0.1.0.dist-info/licenses/LICENSE +21 -0
- behalf-0.1.0.dist-info/licenses/NOTICE +21 -0
- behalf-0.1.0.dist-info/top_level.txt +1 -0
behalf/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""behalf — a small framework for scoped agents.
|
|
2
|
+
|
|
3
|
+
Give an agent a brief, a fixed set of tools (read, or action-with-confirmation),
|
|
4
|
+
and a pluggable model backend; it works within that behalf and returns a result.
|
|
5
|
+
The core is dependency-free; a backend extra (claude/gemini/aws) adds a runner.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core import (
|
|
9
|
+
AgentRunner,
|
|
10
|
+
ConfirmFn,
|
|
11
|
+
RunOutcome,
|
|
12
|
+
Task,
|
|
13
|
+
ToolSpec,
|
|
14
|
+
default_confirm,
|
|
15
|
+
run_task,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AgentRunner", "ConfirmFn", "RunOutcome", "Task", "ToolSpec",
|
|
20
|
+
"default_confirm", "run_task", "make_runner",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_runner(backend: str, model: str | None = None):
|
|
25
|
+
"""Resolve a runner by name, importing only that backend's SDK.
|
|
26
|
+
|
|
27
|
+
backend: "claude" | "gemini" | "aws". Raises SystemExit with an install hint
|
|
28
|
+
if the backend's package isn't installed.
|
|
29
|
+
"""
|
|
30
|
+
if backend == "claude":
|
|
31
|
+
try:
|
|
32
|
+
from .runner.sdk import SDKRunner
|
|
33
|
+
except ImportError as e:
|
|
34
|
+
raise SystemExit(f"backend 'claude' needs the Claude Agent SDK: pip install behalf[claude] ({e})")
|
|
35
|
+
return SDKRunner(model) if model else SDKRunner()
|
|
36
|
+
if backend == "gemini":
|
|
37
|
+
try:
|
|
38
|
+
from .runner.adk import ADKRunner
|
|
39
|
+
except ImportError as e:
|
|
40
|
+
raise SystemExit(f"backend 'gemini' needs Google ADK: pip install behalf[gemini] ({e})")
|
|
41
|
+
return ADKRunner(model=model) if model else ADKRunner()
|
|
42
|
+
if backend == "aws":
|
|
43
|
+
try:
|
|
44
|
+
from .runner.strands import StrandsRunner
|
|
45
|
+
except ImportError as e:
|
|
46
|
+
raise SystemExit(f"backend 'aws' needs Strands: pip install behalf[aws] ({e})")
|
|
47
|
+
return StrandsRunner(model=model) if model else StrandsRunner()
|
|
48
|
+
raise SystemExit(f"unknown backend {backend!r} (choose: claude, gemini, aws)")
|
behalf/console.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Small terminal-output helper: a bit of color and structure for the live
|
|
2
|
+
trace. Colors switch off automatically when stdout isn't a TTY (piped, captured
|
|
3
|
+
in tests) or when NO_COLOR is set, so output stays clean everywhere."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
_ON = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _wrap(code: str):
|
|
14
|
+
if not _ON:
|
|
15
|
+
return lambda s: str(s)
|
|
16
|
+
return lambda s: f"\033[{code}m{s}\033[0m"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
bold = _wrap("1")
|
|
20
|
+
dim = _wrap("2")
|
|
21
|
+
red = _wrap("31")
|
|
22
|
+
green = _wrap("32")
|
|
23
|
+
yellow = _wrap("33")
|
|
24
|
+
cyan = _wrap("36")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def header(text: str, index: int | None = None, total: int | None = None) -> None:
|
|
28
|
+
tag = dim(f" [{index}/{total}]") if index and total else ""
|
|
29
|
+
print(f"\n{cyan('▸')} {bold(text)}{tag}", flush=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def phase(text: str) -> None:
|
|
33
|
+
print(f" {dim('·')} {dim(text)}", flush=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def op(name: str, detail: str = "") -> None:
|
|
37
|
+
tail = f" {dim(detail)}" if detail else ""
|
|
38
|
+
print(f" {green('→')} {bold(name)}{tail}", flush=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ok(text: str) -> None:
|
|
42
|
+
print(f" {green('✓')} {text}", flush=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def warn(text: str) -> None:
|
|
46
|
+
print(f" {yellow('⚠')} {text}", flush=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def think(chunk: str) -> None:
|
|
50
|
+
"""Streamed assistant text, dimmed so the operation lines stand out."""
|
|
51
|
+
sys.stdout.write(dim(chunk))
|
|
52
|
+
sys.stdout.flush()
|
behalf/core.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""A tiny framework for a recurring pattern: give an agent a SCOPED task that
|
|
2
|
+
first gathers intent from the user, freezes it into a reproducible manifest,
|
|
3
|
+
then executes toward one specific outcome with a fixed, typed toolset.
|
|
4
|
+
|
|
5
|
+
elicit (adaptive conversation, seeded) -> manifest (frozen, reviewable)
|
|
6
|
+
-> execute (agent + fixed tools)
|
|
7
|
+
|
|
8
|
+
The core knows nothing about any particular domain. A Task supplies the three
|
|
9
|
+
things that vary: what to ask the user, which tools the agent may use, and what
|
|
10
|
+
a valid result is. Read-only tasks, tasks with an action tool that gates on user
|
|
11
|
+
confirmation, and pipelines of tasks are all just Tasks.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, Awaitable, Callable, Optional, Protocol
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ToolSpec:
|
|
24
|
+
"""A tool the agent can call. `kind` distinguishes read (call freely) from
|
|
25
|
+
action (mutates something -> may require confirming the exact arguments)."""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
description: str
|
|
29
|
+
input_schema: dict
|
|
30
|
+
handler: Callable[[dict], Awaitable[dict]]
|
|
31
|
+
kind: str = "read" # "read" | "action"
|
|
32
|
+
confirm: bool = False # if action: pause for approval before running
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# confirm_fn(tool_name, args) -> bool. Supplied by the runtime (CLI prompt, test
|
|
36
|
+
# stub, auto-approve). Only ever called for action tools with confirm=True.
|
|
37
|
+
ConfirmFn = Callable[[str, dict], bool]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Task(ABC):
|
|
41
|
+
"""A scoped capability. Subclasses fill in the three varying pieces."""
|
|
42
|
+
|
|
43
|
+
name: str = "task"
|
|
44
|
+
|
|
45
|
+
# Whether the frozen setup manifest must be approved before execution.
|
|
46
|
+
# (Action tools have their OWN per-call confirmation regardless.)
|
|
47
|
+
requires_setup_approval: bool = False
|
|
48
|
+
|
|
49
|
+
def setup_system_prompt(self) -> str:
|
|
50
|
+
"""Seeds elicitation: what the agent already knows (schemas, fixed
|
|
51
|
+
metadata) and what it must learn from the user."""
|
|
52
|
+
return "Gather what you need from the user, then finalize a setup manifest."
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def manifest_schema(self) -> dict:
|
|
56
|
+
"""The schema the elicitation must produce and freeze."""
|
|
57
|
+
|
|
58
|
+
def build_tools(self, manifest: dict) -> list[ToolSpec]:
|
|
59
|
+
"""Fixed toolset for the default single-run execute(). Iterating tasks
|
|
60
|
+
override execute() and build tools per item, leaving this as []."""
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def execute_system_prompt(self, manifest: dict) -> str:
|
|
65
|
+
"""Instruction for the execution agent."""
|
|
66
|
+
|
|
67
|
+
async def execute(self, runner: "AgentRunner", manifest: dict, confirm_fn: "ConfirmFn") -> Any:
|
|
68
|
+
"""Run the task. Default: a single agent run over the built tools."""
|
|
69
|
+
tools = self.build_tools(manifest)
|
|
70
|
+
return await runner.run_agent(
|
|
71
|
+
system_prompt=self.execute_system_prompt(manifest),
|
|
72
|
+
user_prompt=manifest.get("goal", "Complete the task."),
|
|
73
|
+
tools=tools,
|
|
74
|
+
confirm_fn=confirm_fn,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def validate_result(self, result: Any) -> None:
|
|
78
|
+
"""Raise if the produced result is unacceptable. Override per task."""
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AgentRunner(Protocol):
|
|
83
|
+
"""How the framework talks to a model. Real ones wrap an SDK; tests inject a
|
|
84
|
+
fake. This seam is what lets the whole flow be tested without a key."""
|
|
85
|
+
|
|
86
|
+
async def converse(self, task: "Task") -> dict:
|
|
87
|
+
"""Run the seeded setup conversation; return the manifest."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
async def run_agent(
|
|
91
|
+
self, system_prompt: str, user_prompt: str, tools: list[ToolSpec], confirm_fn: "ConfirmFn"
|
|
92
|
+
) -> Any:
|
|
93
|
+
"""One agent run with a fixed toolset; action tools gate on confirm_fn."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class RunOutcome:
|
|
99
|
+
manifest: dict
|
|
100
|
+
result: Any
|
|
101
|
+
approved: bool = True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def default_confirm(tool_name: str, args: dict) -> bool:
|
|
105
|
+
"""CLI confirmation: show the exact call and ask. Safe default for actions."""
|
|
106
|
+
print(f"\n[confirm] about to run action '{tool_name}' with:")
|
|
107
|
+
print(json.dumps(args, indent=2))
|
|
108
|
+
return input("proceed? [y/N] ").strip().lower() in ("y", "yes")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def run_task(
|
|
112
|
+
task: Task,
|
|
113
|
+
runner: AgentRunner,
|
|
114
|
+
manifest: Optional[dict] = None,
|
|
115
|
+
confirm_fn: ConfirmFn = default_confirm,
|
|
116
|
+
approve_fn: Optional[Callable[[dict], bool]] = None,
|
|
117
|
+
) -> RunOutcome:
|
|
118
|
+
"""The one entrypoint every task flows through. manifest given -> skip the
|
|
119
|
+
conversation (reproducible/batch); None -> converse to produce it. Then
|
|
120
|
+
optional setup approval, then execute; action tools gate on confirm_fn."""
|
|
121
|
+
if manifest is None:
|
|
122
|
+
manifest = await runner.converse(task)
|
|
123
|
+
|
|
124
|
+
approved = True
|
|
125
|
+
if task.requires_setup_approval:
|
|
126
|
+
approver = approve_fn or (lambda m: default_confirm("finalize-setup", m))
|
|
127
|
+
approved = approver(manifest)
|
|
128
|
+
if not approved:
|
|
129
|
+
return RunOutcome(manifest=manifest, result=None, approved=False)
|
|
130
|
+
|
|
131
|
+
result = await task.execute(runner, manifest, confirm_fn)
|
|
132
|
+
task.validate_result(result)
|
|
133
|
+
return RunOutcome(manifest=manifest, result=result, approved=approved)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Model backends. Each submodule imports its own SDK at module load, so nothing
|
|
2
|
+
here is imported until you ask for a specific runner — installing one backend's
|
|
3
|
+
extra doesn't drag in the others.
|
|
4
|
+
|
|
5
|
+
`from behalf.runner import SDKRunner` resolves lazily; importing the submodule
|
|
6
|
+
directly (`behalf.runner.sdk`) is equivalent.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
_RUNNERS = {
|
|
10
|
+
"SDKRunner": ".sdk", # Claude, via claude-agent-sdk
|
|
11
|
+
"ADKRunner": ".adk", # Gemini, via google-adk
|
|
12
|
+
"StrandsRunner": ".strands", # AWS Bedrock, via strands-agents
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __getattr__(name):
|
|
17
|
+
module = _RUNNERS.get(name)
|
|
18
|
+
if module is None:
|
|
19
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
20
|
+
from importlib import import_module
|
|
21
|
+
|
|
22
|
+
return getattr(import_module(module, __name__), name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def __dir__():
|
|
26
|
+
return sorted(_RUNNERS)
|
behalf/runner/adk.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""A Gemini backend via Google's Agent Development Kit (ADK), as an alternative
|
|
2
|
+
AgentRunner to SDKRunner.
|
|
3
|
+
|
|
4
|
+
STATUS: scaffolded, NOT yet run. Google ADK isn't a base dependency and isn't
|
|
5
|
+
installed in the dev image, so this module is untested end to end. The shape
|
|
6
|
+
follows ADK's documented API (Agent + FunctionTool + Runner + session service),
|
|
7
|
+
but the spots marked TODO are where the ADK specifics must be confirmed against
|
|
8
|
+
the installed version before relying on it. Install with `pip install google-adk`.
|
|
9
|
+
|
|
10
|
+
It exists because the framework already abstracts the model behind AgentRunner,
|
|
11
|
+
so adding Gemini is a contained job: convert our ToolSpecs to ADK function tools
|
|
12
|
+
(carrying the same read/action confirmation gate) and drive ADK's runner. None
|
|
13
|
+
of ProfileTask / tools.py / the container code changes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import inspect
|
|
19
|
+
import json
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# Top-level imports so a missing ADK fails here, at import, not mid-run. This
|
|
23
|
+
# module is only imported when you explicitly want the Gemini path
|
|
24
|
+
# (`from behalf.runner.adk import ADKRunner`), so it never burdens the
|
|
25
|
+
# Claude/SDK path or `import behalf`.
|
|
26
|
+
from google.adk.agents import Agent
|
|
27
|
+
from google.adk.runners import Runner
|
|
28
|
+
from google.adk.sessions import InMemorySessionService
|
|
29
|
+
from google.adk.tools import FunctionTool
|
|
30
|
+
from google.genai import types
|
|
31
|
+
|
|
32
|
+
from ..core import AgentRunner, ConfirmFn, Task, ToolSpec
|
|
33
|
+
|
|
34
|
+
_PY_TYPE = {str: str, int: int, float: float, bool: bool, list: list, dict: dict}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _to_adk_tool(ts: ToolSpec, confirm_fn: ConfirmFn) -> FunctionTool:
|
|
38
|
+
"""Wrap one ToolSpec as an ADK FunctionTool.
|
|
39
|
+
|
|
40
|
+
ADK builds a tool's schema from the wrapped function's SIGNATURE (typed
|
|
41
|
+
params + docstring), whereas our ToolSpec carries an input_schema dict. So we
|
|
42
|
+
synthesize a function with matching parameters, then let ADK introspect it.
|
|
43
|
+
|
|
44
|
+
The wrapper also applies the read/action confirmation gate (same semantics as
|
|
45
|
+
SDKRunner) and adapts our handler's Anthropic-style {"content":[...]} return
|
|
46
|
+
into the plain value ADK expects a tool to return.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
async def _invoke(**kwargs) -> Any:
|
|
50
|
+
if ts.kind == "action" and ts.confirm and not confirm_fn(ts.name, kwargs):
|
|
51
|
+
return "cancelled by user"
|
|
52
|
+
result = await ts.handler(kwargs)
|
|
53
|
+
# our handlers return {"content": [{"type": "text", "text": "..."}]}
|
|
54
|
+
try:
|
|
55
|
+
text = result["content"][0]["text"]
|
|
56
|
+
except (KeyError, IndexError, TypeError):
|
|
57
|
+
return result
|
|
58
|
+
try:
|
|
59
|
+
return json.loads(text) # most of our tools emit JSON
|
|
60
|
+
except (ValueError, TypeError):
|
|
61
|
+
return text
|
|
62
|
+
|
|
63
|
+
# give ADK a real signature + docstring to introspect
|
|
64
|
+
params = [
|
|
65
|
+
inspect.Parameter(
|
|
66
|
+
name, inspect.Parameter.KEYWORD_ONLY, annotation=_PY_TYPE.get(typ, str)
|
|
67
|
+
)
|
|
68
|
+
for name, typ in ts.input_schema.items()
|
|
69
|
+
]
|
|
70
|
+
_invoke.__name__ = ts.name
|
|
71
|
+
_invoke.__doc__ = ts.description
|
|
72
|
+
_invoke.__signature__ = inspect.Signature(
|
|
73
|
+
params
|
|
74
|
+
) # TODO: confirm ADK reads __signature__ (vs real params)
|
|
75
|
+
_invoke.__annotations__ = {
|
|
76
|
+
n: _PY_TYPE.get(t, str) for n, t in ts.input_schema.items()
|
|
77
|
+
}
|
|
78
|
+
return FunctionTool(
|
|
79
|
+
_invoke
|
|
80
|
+
) # TODO: confirm FunctionTool(func) is the current constructor
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ADKRunner(AgentRunner):
|
|
84
|
+
"""AgentRunner backed by Google ADK / Gemini. Same interface as SDKRunner."""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
model: str = "gemini-2.5-flash",
|
|
89
|
+
app_name: str = "behalf",
|
|
90
|
+
verbose: bool = True,
|
|
91
|
+
):
|
|
92
|
+
self.model = model
|
|
93
|
+
self.app_name = app_name
|
|
94
|
+
self.verbose = verbose
|
|
95
|
+
|
|
96
|
+
async def _run_once(
|
|
97
|
+
self, instruction: str, user_prompt: str, adk_tools: list
|
|
98
|
+
) -> list[str]:
|
|
99
|
+
agent = Agent(
|
|
100
|
+
name="behalf", model=self.model, instruction=instruction, tools=adk_tools
|
|
101
|
+
)
|
|
102
|
+
session_service = InMemorySessionService()
|
|
103
|
+
# TODO: confirm create_session is sync vs async in the installed ADK
|
|
104
|
+
session = session_service.create_session(
|
|
105
|
+
app_name=self.app_name, user_id="local", session_id="s1"
|
|
106
|
+
)
|
|
107
|
+
runner = Runner(
|
|
108
|
+
agent=agent, app_name=self.app_name, session_service=session_service
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
msg = types.Content(role="user", parts=[types.Part(text=user_prompt)])
|
|
112
|
+
texts: list[str] = []
|
|
113
|
+
# TODO: confirm run_async signature + how final text is surfaced on events
|
|
114
|
+
async for event in runner.run_async(
|
|
115
|
+
user_id="local", session_id=session.id, new_message=msg
|
|
116
|
+
):
|
|
117
|
+
if self.verbose:
|
|
118
|
+
print(event)
|
|
119
|
+
content = getattr(event, "content", None)
|
|
120
|
+
if content and getattr(content, "parts", None):
|
|
121
|
+
for part in content.parts:
|
|
122
|
+
if getattr(part, "text", None):
|
|
123
|
+
texts.append(part.text)
|
|
124
|
+
return texts
|
|
125
|
+
|
|
126
|
+
async def run_agent(
|
|
127
|
+
self,
|
|
128
|
+
system_prompt: str,
|
|
129
|
+
user_prompt: str,
|
|
130
|
+
tools: list[ToolSpec],
|
|
131
|
+
confirm_fn: ConfirmFn,
|
|
132
|
+
) -> Any:
|
|
133
|
+
adk_tools = [_to_adk_tool(t, confirm_fn) for t in tools]
|
|
134
|
+
await self._run_once(system_prompt, user_prompt, adk_tools)
|
|
135
|
+
# results are collected via the tools' side effects (e.g. record_artifact
|
|
136
|
+
# writing into ProfileTask's sink), same as SDKRunner.
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
async def converse(self, task: Task) -> dict:
|
|
140
|
+
"""Interactive setup: give the agent a finalize tool and loop until it's
|
|
141
|
+
called. Mirrors SDKRunner.converse. TODO: verify ADK multi-turn session
|
|
142
|
+
reuse (re-running the runner with the same session_id to continue)."""
|
|
143
|
+
captured: dict = {}
|
|
144
|
+
|
|
145
|
+
async def finalize_setup(manifest: dict) -> str:
|
|
146
|
+
"""Finalize the setup manifest (matching the task's schema) and end setup."""
|
|
147
|
+
captured["manifest"] = manifest
|
|
148
|
+
return "setup finalized"
|
|
149
|
+
|
|
150
|
+
finalize = FunctionTool(finalize_setup)
|
|
151
|
+
agent = Agent(
|
|
152
|
+
name="setup",
|
|
153
|
+
model=self.model,
|
|
154
|
+
instruction=task.setup_system_prompt(),
|
|
155
|
+
tools=[finalize],
|
|
156
|
+
)
|
|
157
|
+
session_service = InMemorySessionService()
|
|
158
|
+
session = session_service.create_session(
|
|
159
|
+
app_name=self.app_name, user_id="local", session_id="setup"
|
|
160
|
+
)
|
|
161
|
+
runner = Runner(
|
|
162
|
+
agent=agent, app_name=self.app_name, session_service=session_service
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
prompt = "Let's set up this task. Ask me what you need."
|
|
166
|
+
while "manifest" not in captured:
|
|
167
|
+
msg = types.Content(role="user", parts=[types.Part(text=prompt)])
|
|
168
|
+
async for event in runner.run_async(
|
|
169
|
+
user_id="local", session_id=session.id, new_message=msg
|
|
170
|
+
):
|
|
171
|
+
if self.verbose:
|
|
172
|
+
print(event)
|
|
173
|
+
if "manifest" in captured:
|
|
174
|
+
break
|
|
175
|
+
prompt = input("\nyou> ")
|
|
176
|
+
return captured["manifest"]
|
behalf/runner/sdk.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Claude backend: an AgentRunner backed by the Claude Agent SDK.
|
|
2
|
+
|
|
3
|
+
The SDK provides the agent loop, tool dispatch, and permissions. This module
|
|
4
|
+
adapts our ToolSpecs into SDK tools (adding the action-confirmation gate) and
|
|
5
|
+
runs them two ways:
|
|
6
|
+
|
|
7
|
+
run_agent() one execution pass over a fixed toolset
|
|
8
|
+
converse() interactive setup that ends when the model calls finalize_setup
|
|
9
|
+
|
|
10
|
+
Live use needs ANTHROPIC_API_KEY and the Claude Code CLI on PATH.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from claude_agent_sdk import (
|
|
18
|
+
ClaudeAgentOptions,
|
|
19
|
+
ClaudeSDKClient,
|
|
20
|
+
create_sdk_mcp_server,
|
|
21
|
+
query,
|
|
22
|
+
tool,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from ..core import AgentRunner, ConfirmFn, Task, ToolSpec
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _say(text: str) -> dict:
|
|
29
|
+
"""An SDK tool result carrying a single text block."""
|
|
30
|
+
return {"content": [{"type": "text", "text": text}]}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _to_sdk_tool(spec: ToolSpec, confirm_fn: ConfirmFn):
|
|
34
|
+
"""Adapt one ToolSpec into an SDK tool. Action tools ask for confirmation of
|
|
35
|
+
their exact arguments before the handler runs; read tools run directly."""
|
|
36
|
+
|
|
37
|
+
@tool(spec.name, spec.description, spec.input_schema)
|
|
38
|
+
async def _sdk_tool(args):
|
|
39
|
+
if spec.kind == "action" and spec.confirm and not confirm_fn(spec.name, args):
|
|
40
|
+
return _say("cancelled by user")
|
|
41
|
+
return await spec.handler(args)
|
|
42
|
+
|
|
43
|
+
return _sdk_tool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SDKRunner(AgentRunner):
|
|
47
|
+
def __init__(self, model: str | None = None, verbose: bool = True):
|
|
48
|
+
self.model = model
|
|
49
|
+
self.verbose = verbose
|
|
50
|
+
|
|
51
|
+
def _echo(self, message: Any) -> None:
|
|
52
|
+
if self.verbose:
|
|
53
|
+
print(message)
|
|
54
|
+
|
|
55
|
+
def _options(
|
|
56
|
+
self, server_name: str, server, allowed: list[str], system_prompt: str
|
|
57
|
+
) -> ClaudeAgentOptions:
|
|
58
|
+
# Our tools are read-only or already gated by confirm_fn, so the SDK's
|
|
59
|
+
# own permission prompts are redundant here.
|
|
60
|
+
return ClaudeAgentOptions(
|
|
61
|
+
mcp_servers={server_name: server},
|
|
62
|
+
allowed_tools=allowed,
|
|
63
|
+
system_prompt=system_prompt,
|
|
64
|
+
permission_mode="bypassPermissions",
|
|
65
|
+
model=self.model,
|
|
66
|
+
max_turns=40,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def run_agent(
|
|
70
|
+
self,
|
|
71
|
+
system_prompt: str,
|
|
72
|
+
user_prompt: str,
|
|
73
|
+
tools: list[ToolSpec],
|
|
74
|
+
confirm_fn: ConfirmFn,
|
|
75
|
+
) -> Any:
|
|
76
|
+
server = create_sdk_mcp_server(
|
|
77
|
+
name="behalf",
|
|
78
|
+
version="0.1.0",
|
|
79
|
+
tools=[_to_sdk_tool(t, confirm_fn) for t in tools],
|
|
80
|
+
)
|
|
81
|
+
allowed = [f"mcp__behalf__{t.name}" for t in tools]
|
|
82
|
+
options = self._options("behalf", server, allowed, system_prompt)
|
|
83
|
+
|
|
84
|
+
async for message in query(prompt=user_prompt, options=options):
|
|
85
|
+
self._echo(message)
|
|
86
|
+
# Results are collected through the tools' side effects (e.g.
|
|
87
|
+
# record_artifact writing into the task's sink), not the return value.
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
async def converse(self, task: Task) -> dict:
|
|
91
|
+
captured: dict = {}
|
|
92
|
+
|
|
93
|
+
@tool(
|
|
94
|
+
"finalize_setup",
|
|
95
|
+
"Finalize the setup manifest (matching the task's schema) and end setup.",
|
|
96
|
+
{"manifest": dict},
|
|
97
|
+
)
|
|
98
|
+
async def finalize(args):
|
|
99
|
+
captured["manifest"] = args.get("manifest", args)
|
|
100
|
+
return _say("setup finalized")
|
|
101
|
+
|
|
102
|
+
server = create_sdk_mcp_server(name="setup", version="0.1.0", tools=[finalize])
|
|
103
|
+
options = self._options(
|
|
104
|
+
"setup", server, ["mcp__setup__finalize_setup"], task.setup_system_prompt()
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# The model asks the user for what it needs and drives the structure; we
|
|
108
|
+
# relay each of the user's replies until it calls finalize_setup.
|
|
109
|
+
async with ClaudeSDKClient(options=options) as client:
|
|
110
|
+
await client.query("Let's set up this task. Ask me what you need.")
|
|
111
|
+
while "manifest" not in captured:
|
|
112
|
+
async for message in client.receive_response():
|
|
113
|
+
self._echo(message)
|
|
114
|
+
if "manifest" not in captured:
|
|
115
|
+
await client.query(input("\nyou> "))
|
|
116
|
+
return captured["manifest"]
|
behalf/runner/strands.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""An AWS backend via the Strands Agents SDK, running against Amazon Bedrock.
|
|
2
|
+
|
|
3
|
+
Handy when Bedrock (IAM/AWS credentials) is easier to reach than an Anthropic or
|
|
4
|
+
Google key. Authenticates with your normal AWS credential chain (profile / env /
|
|
5
|
+
role) and a region; the model is a Bedrock model id, which can still be Claude
|
|
6
|
+
(a Bedrock Anthropic inference profile), just reached through Bedrock.
|
|
7
|
+
|
|
8
|
+
STATUS: scaffolded, NOT yet run here (needs `pip install strands-agents` and AWS
|
|
9
|
+
creds). Shape follows the documented Strands API; spots to confirm against the
|
|
10
|
+
installed version are marked TODO. Strands tools are plain Python functions and
|
|
11
|
+
it exposes tool calls, so our ToolSpec -> function conversion (with the
|
|
12
|
+
read/action confirm gate) maps over directly.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import inspect
|
|
19
|
+
import json
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from strands import Agent
|
|
23
|
+
from strands.models import (
|
|
24
|
+
BedrockModel,
|
|
25
|
+
) # TODO: confirm class name in the installed version
|
|
26
|
+
|
|
27
|
+
from ..core import AgentRunner, ConfirmFn, Task, ToolSpec
|
|
28
|
+
|
|
29
|
+
_PY_TYPE = {str: str, int: int, float: float, bool: bool, list: list, dict: dict}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _op_detail(name: str, kwargs: dict) -> str:
|
|
33
|
+
"""A short, domain-agnostic one-liner for the trace: the scalar arguments,
|
|
34
|
+
space-joined. (A caller that wants prettier per-tool formatting can wrap the
|
|
35
|
+
runner; the library stays generic.)"""
|
|
36
|
+
scalars = [str(v) for v in kwargs.values()
|
|
37
|
+
if isinstance(v, (str, int, float, bool)) and str(v) != ""]
|
|
38
|
+
detail = " ".join(scalars) if scalars else json.dumps(kwargs, default=str)
|
|
39
|
+
return detail[:97] + "..." if len(detail) > 100 else detail
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _to_strands_tool(ts: ToolSpec, confirm_fn: ConfirmFn, on_call=None):
|
|
43
|
+
"""ToolSpec -> Strands tool. Strands reads the function signature + docstring
|
|
44
|
+
for the schema, so we synthesize typed params. Our async handler is bridged
|
|
45
|
+
to sync, the confirm gate is applied for action tools, and the
|
|
46
|
+
{"content":[...]} return is adapted to a plain value. on_call, if given, is
|
|
47
|
+
invoked with (name, kwargs) when the tool actually runs — that's where the
|
|
48
|
+
real-time trace line comes from, since the arguments are complete by then."""
|
|
49
|
+
from strands import tool
|
|
50
|
+
|
|
51
|
+
def _invoke(**kwargs) -> Any:
|
|
52
|
+
if on_call is not None:
|
|
53
|
+
on_call(ts.name, kwargs)
|
|
54
|
+
if ts.kind == "action" and ts.confirm and not confirm_fn(ts.name, kwargs):
|
|
55
|
+
return "cancelled by user"
|
|
56
|
+
result = asyncio.run(ts.handler(kwargs))
|
|
57
|
+
try:
|
|
58
|
+
text = result["content"][0]["text"]
|
|
59
|
+
except (KeyError, IndexError, TypeError):
|
|
60
|
+
return result
|
|
61
|
+
try:
|
|
62
|
+
return json.loads(text)
|
|
63
|
+
except (ValueError, TypeError):
|
|
64
|
+
return text
|
|
65
|
+
|
|
66
|
+
_invoke.__name__ = ts.name
|
|
67
|
+
_invoke.__doc__ = ts.description
|
|
68
|
+
_invoke.__signature__ = inspect.Signature(
|
|
69
|
+
[
|
|
70
|
+
inspect.Parameter(
|
|
71
|
+
n, inspect.Parameter.KEYWORD_ONLY, annotation=_PY_TYPE.get(t, str)
|
|
72
|
+
)
|
|
73
|
+
for n, t in ts.input_schema.items()
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
_invoke.__annotations__ = {
|
|
77
|
+
n: _PY_TYPE.get(t, str) for n, t in ts.input_schema.items()
|
|
78
|
+
}
|
|
79
|
+
return tool(_invoke) # TODO: confirm strands.tool(func) call form
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StrandsRunner(AgentRunner):
|
|
83
|
+
"""AgentRunner backed by Strands + Bedrock. Same interface as SDKRunner."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self, model: str | None = None, region: str = "us-east-1", verbose: bool = True
|
|
87
|
+
):
|
|
88
|
+
self.model = model # None -> Strands' default (an Anthropic model on Bedrock)
|
|
89
|
+
self.region = region
|
|
90
|
+
self.verbose = verbose
|
|
91
|
+
|
|
92
|
+
def _model(self):
|
|
93
|
+
if self.model is None:
|
|
94
|
+
return None
|
|
95
|
+
return BedrockModel(
|
|
96
|
+
model_id=self.model, region_name=self.region
|
|
97
|
+
) # TODO: confirm kwargs
|
|
98
|
+
|
|
99
|
+
def _render(self, event) -> None:
|
|
100
|
+
"""Stream assistant text (dimmed). Tool lines print from the wrapper."""
|
|
101
|
+
if self.verbose and isinstance(event, dict) and event.get("data"):
|
|
102
|
+
from .. import console
|
|
103
|
+
|
|
104
|
+
console.think(event["data"])
|
|
105
|
+
self._mid_line = not event["data"].endswith("\n")
|
|
106
|
+
|
|
107
|
+
def _trace(self, name: str, kwargs: dict) -> None:
|
|
108
|
+
if not self.verbose:
|
|
109
|
+
return
|
|
110
|
+
from .. import console
|
|
111
|
+
|
|
112
|
+
if getattr(self, "_mid_line", False):
|
|
113
|
+
print() # close a partial line of streamed text before the op line
|
|
114
|
+
self._mid_line = False
|
|
115
|
+
console.op(name, _op_detail(name, kwargs))
|
|
116
|
+
|
|
117
|
+
async def run_agent(
|
|
118
|
+
self,
|
|
119
|
+
system_prompt: str,
|
|
120
|
+
user_prompt: str,
|
|
121
|
+
tools: list[ToolSpec],
|
|
122
|
+
confirm_fn: ConfirmFn,
|
|
123
|
+
) -> Any:
|
|
124
|
+
self._mid_line = False
|
|
125
|
+
kwargs = {
|
|
126
|
+
"system_prompt": system_prompt,
|
|
127
|
+
"tools": [
|
|
128
|
+
_to_strands_tool(t, confirm_fn, on_call=self._trace) for t in tools
|
|
129
|
+
],
|
|
130
|
+
# silence Strands' own stdout printer so it doesn't double with ours
|
|
131
|
+
"callback_handler": None,
|
|
132
|
+
}
|
|
133
|
+
model = self._model()
|
|
134
|
+
if model is not None:
|
|
135
|
+
kwargs["model"] = model
|
|
136
|
+
agent = Agent(**kwargs)
|
|
137
|
+
|
|
138
|
+
stream = getattr(agent, "stream_async", None)
|
|
139
|
+
if stream is None:
|
|
140
|
+
result = await asyncio.to_thread(
|
|
141
|
+
agent, user_prompt
|
|
142
|
+
) # TODO: prefer invoke_async
|
|
143
|
+
if self.verbose:
|
|
144
|
+
print(result)
|
|
145
|
+
else:
|
|
146
|
+
async for event in stream(user_prompt):
|
|
147
|
+
self._render(event)
|
|
148
|
+
if self.verbose and self._mid_line:
|
|
149
|
+
print() # newline after trailing streamed text
|
|
150
|
+
return None # results collected via tool side effects (record_artifact -> sink)
|
|
151
|
+
|
|
152
|
+
async def converse(self, task: Task) -> dict:
|
|
153
|
+
from strands import tool
|
|
154
|
+
|
|
155
|
+
captured: dict = {}
|
|
156
|
+
|
|
157
|
+
def finalize_setup(manifest: dict) -> str:
|
|
158
|
+
"""Finalize the setup manifest (matching the task's schema) and end setup."""
|
|
159
|
+
captured["manifest"] = manifest
|
|
160
|
+
return "setup finalized"
|
|
161
|
+
|
|
162
|
+
kwargs = {
|
|
163
|
+
"system_prompt": task.setup_system_prompt(),
|
|
164
|
+
"tools": [tool(finalize_setup)],
|
|
165
|
+
}
|
|
166
|
+
model = self._model()
|
|
167
|
+
if model is not None:
|
|
168
|
+
kwargs["model"] = model
|
|
169
|
+
agent = Agent(**kwargs)
|
|
170
|
+
|
|
171
|
+
prompt = "Let's set up this task. Ask me what you need."
|
|
172
|
+
while "manifest" not in captured:
|
|
173
|
+
out = await asyncio.to_thread(agent, prompt)
|
|
174
|
+
if self.verbose:
|
|
175
|
+
print(out)
|
|
176
|
+
if "manifest" in captured:
|
|
177
|
+
break
|
|
178
|
+
prompt = input("\nyou> ")
|
|
179
|
+
return captured["manifest"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: behalf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small framework for scoped agents: a brief, a typed toolset (read/action), a pluggable model backend.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: NOTICE
|
|
10
|
+
Provides-Extra: claude
|
|
11
|
+
Requires-Dist: claude-agent-sdk>=0.2; extra == "claude"
|
|
12
|
+
Provides-Extra: gemini
|
|
13
|
+
Requires-Dist: google-adk>=1.0; extra == "gemini"
|
|
14
|
+
Provides-Extra: aws
|
|
15
|
+
Requires-Dist: strands-agents>=0.1; extra == "aws"
|
|
16
|
+
Requires-Dist: boto3>=1.34; extra == "aws"
|
|
17
|
+
Provides-Extra: all
|
|
18
|
+
Requires-Dist: claude-agent-sdk>=0.2; extra == "all"
|
|
19
|
+
Requires-Dist: google-adk>=1.0; extra == "all"
|
|
20
|
+
Requires-Dist: strands-agents>=0.1; extra == "all"
|
|
21
|
+
Requires-Dist: boto3>=1.34; extra == "all"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# behalf
|
|
27
|
+
|
|
28
|
+
A small framework for the recurring pattern of a **scoped agent**: hand it a
|
|
29
|
+
brief, a fixed set of tools, and bounded authority, and it does one job.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Abstractions
|
|
33
|
+
|
|
34
|
+
- **Task** what to elicit from the user, which tools the agent gets, and what a valid result is.
|
|
35
|
+
- **ToolSpec** can be a single tool or action.
|
|
36
|
+
- **runner backend** is the model like the Claude Agent SDK, Google ADK, or Strands on Bedrock.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from behalf import run_task, make_runner
|
|
41
|
+
outcome = await run_task(MyTask(), make_runner("aws"))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install behalf # core only (no backend)
|
|
49
|
+
pip install behalf[claude] # + Claude Agent SDK
|
|
50
|
+
pip install behalf[aws] # + Strands / Bedrock
|
|
51
|
+
pip install behalf[gemini] # + Google ADK
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
HPCIC DevTools is distributed under the terms of the MIT license.
|
|
57
|
+
All new contributions must be made under this license.
|
|
58
|
+
|
|
59
|
+
See [LICENSE](https://github.com/converged-computing/cloud-select/blob/main/LICENSE),
|
|
60
|
+
[COPYRIGHT](https://github.com/converged-computing/cloud-select/blob/main/COPYRIGHT), and
|
|
61
|
+
[NOTICE](https://github.com/converged-computing/cloud-select/blob/main/NOTICE) for details.
|
|
62
|
+
|
|
63
|
+
SPDX-License-Identifier: (MIT)
|
|
64
|
+
|
|
65
|
+
LLNL-CODE- 842614
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
behalf/__init__.py,sha256=vuUyk4YWtnYXYczwUPFOv3TJZ7pn5080Zo7kj-vShBA,1768
|
|
2
|
+
behalf/console.py,sha256=c77STgrmzq1zrSXFYz3x8OFLSkh_RdayzNl1u-o8D5g,1361
|
|
3
|
+
behalf/core.py,sha256=akrlYi6ekGIh3sE4LnPn1vwaTtBBkHWOkC_2yUpPGYc,5096
|
|
4
|
+
behalf/runner/__init__.py,sha256=ck54cntAUFu_HLoFC8DIWcYrwhL_-fkuVDGoMgQNRKw,832
|
|
5
|
+
behalf/runner/adk.py,sha256=0ofUAGUfOFz3t6Md-klbIbT_bT12GvMeBPSPrJndXIg,6968
|
|
6
|
+
behalf/runner/sdk.py,sha256=E1KmGUXD9tAeDwzFlv9OsPxT8cn232Gf601HEQxQ7Co,4089
|
|
7
|
+
behalf/runner/strands.py,sha256=XNdk4QCy9dRuSwRcVoeUde8J2_0SSXkS3Wat2lbVbR4,6661
|
|
8
|
+
behalf-0.1.0.dist-info/licenses/LICENSE,sha256=AlyLB1m_z0CENCx1ob0PedLTTohtH2VLZhs2kfygrfc,1108
|
|
9
|
+
behalf-0.1.0.dist-info/licenses/NOTICE,sha256=9CR93geVKl_4ZrJORbXN0fzkEM2y4DglWhY1hn9ZwQw,1167
|
|
10
|
+
behalf-0.1.0.dist-info/METADATA,sha256=6Qq0OjqJzDxRyOHCLNpQ7jzp5M99kPQHvMywAexyJKU,2061
|
|
11
|
+
behalf-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
12
|
+
behalf-0.1.0.dist-info/top_level.txt,sha256=8tZaLJc5ZkQscR9dyCnXL9XgXupfr8DtB2Irt_C7QLs,7
|
|
13
|
+
behalf-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2023 LLNS, LLC and other HPCIC DevTools Developers.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
This work was produced under the auspices of the U.S. Department of
|
|
2
|
+
Energy by Lawrence Livermore National Laboratory under Contract
|
|
3
|
+
DE-AC52-07NA27344.
|
|
4
|
+
|
|
5
|
+
This work was prepared as an account of work sponsored by an agency of
|
|
6
|
+
the United States Government. Neither the United States Government nor
|
|
7
|
+
Lawrence Livermore National Security, LLC, nor any of their employees
|
|
8
|
+
makes any warranty, expressed or implied, or assumes any legal liability
|
|
9
|
+
or responsibility for the accuracy, completeness, or usefulness of any
|
|
10
|
+
information, apparatus, product, or process disclosed, or represents that
|
|
11
|
+
its use would not infringe privately owned rights.
|
|
12
|
+
|
|
13
|
+
Reference herein to any specific commercial product, process, or service
|
|
14
|
+
by trade name, trademark, manufacturer, or otherwise does not necessarily
|
|
15
|
+
constitute or imply its endorsement, recommendation, or favoring by the
|
|
16
|
+
United States Government or Lawrence Livermore National Security, LLC.
|
|
17
|
+
|
|
18
|
+
The views and opinions of authors expressed herein do not necessarily
|
|
19
|
+
state or reflect those of the United States Government or Lawrence
|
|
20
|
+
Livermore National Security, LLC, and shall not be used for advertising
|
|
21
|
+
or product endorsement purposes.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
behalf
|