@kontourai/flow-agents 0.1.1 → 0.2.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/.github/dependabot.yml +23 -0
- package/.github/workflows/publish-npm.yml +1 -1
- package/.github/workflows/release-please.yml +31 -0
- package/.github/workflows/runtime-compat.yml +118 -0
- package/CHANGELOG.md +38 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +58 -19
- package/build/src/cli/init.js +215 -5
- package/build/src/cli/utterance-check.js +236 -0
- package/build/src/cli.js +3 -0
- package/build/src/tools/build-universal-bundles.js +268 -0
- package/build/src/tools/filter-installed-packs.js +3 -0
- package/build/src/tools/validate-source-tree.js +6 -1
- package/context/scripts/telemetry/lib/config.sh +5 -1
- package/context/settings/flow-agents-settings.json +7 -0
- package/docs/agent-system-guidebook.md +4 -5
- package/docs/context-map.md +1 -0
- package/docs/index.md +46 -6
- package/docs/integrations/conformance.md +246 -0
- package/docs/integrations/framework-adapter.md +275 -0
- package/docs/integrations/harness-install.md +213 -0
- package/docs/integrations/index.md +54 -0
- package/docs/north-star.md +3 -3
- package/docs/repository-structure.md +1 -1
- package/docs/skills-map.md +10 -4
- package/docs/spec/runtime-hook-surface.md +472 -0
- package/docs/survey-utterance-check.md +308 -0
- package/docs/vision.md +45 -0
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/acceptance/run.sh +4 -2
- package/evals/acceptance/test_opencode_harness.sh +121 -0
- package/evals/acceptance/test_pi_harness.sh +98 -0
- package/evals/integration/test_bundle_install.sh +226 -1
- package/evals/integration/test_bundle_lifecycle.sh +641 -0
- package/evals/integration/test_utterance_check.sh +518 -0
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +137 -2
- package/integrations/strands/README.md +256 -0
- package/integrations/strands/example.py +74 -0
- package/integrations/strands/flow_agents_strands/__init__.py +27 -0
- package/integrations/strands/flow_agents_strands/hooks.py +194 -0
- package/integrations/strands/flow_agents_strands/policy.py +348 -0
- package/integrations/strands/flow_agents_strands/steering.py +172 -0
- package/integrations/strands/flow_agents_strands/telemetry.py +238 -0
- package/integrations/strands/pyproject.toml +38 -0
- package/integrations/strands/tests/__init__.py +0 -0
- package/integrations/strands/tests/test_hooks.py +304 -0
- package/integrations/strands/tests/test_policy.py +315 -0
- package/integrations/strands/tests/test_telemetry.py +184 -0
- package/integrations/strands-ts/README.md +224 -0
- package/integrations/strands-ts/bin/conformance-shim.mjs +257 -0
- package/integrations/strands-ts/package.json +53 -0
- package/integrations/strands-ts/src/hooks.ts +208 -0
- package/integrations/strands-ts/src/index.ts +22 -0
- package/integrations/strands-ts/src/policy.ts +345 -0
- package/integrations/strands-ts/src/telemetry.ts +251 -0
- package/integrations/strands-ts/test/test-policy.ts +322 -0
- package/integrations/strands-ts/test/test-telemetry.ts +226 -0
- package/integrations/strands-ts/tsconfig.json +20 -0
- package/package.json +7 -2
- package/packaging/conformance/README.md +142 -0
- package/packaging/conformance/fixtures/config-protection--allow-no-path.json +18 -0
- package/packaging/conformance/fixtures/config-protection--allow-safe-file.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-biome.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-eslintrc.json +20 -0
- package/packaging/conformance/fixtures/quality-gate--allow-no-path.json +17 -0
- package/packaging/conformance/fixtures/quality-gate--allow-nonexistent-file.json +19 -0
- package/packaging/conformance/fixtures/stop-goal-fit--allow-clean-cwd.json +17 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-strict-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +21 -0
- package/packaging/conformance/fixtures/workflow-steering--allow-no-state.json +16 -0
- package/packaging/conformance/fixtures/workflow-steering--inject-active-state.json +29 -0
- package/packaging/conformance/fixtures/workflow-steering--inject-subagent-steering.json +25 -0
- package/packaging/conformance/package.json +4 -0
- package/packaging/conformance/run-conformance.js +322 -0
- package/packaging/manifest.json +59 -0
- package/schemas/flow-agents-settings.schema.json +48 -0
- package/scripts/README.md +5 -0
- package/scripts/dogfood.js +16 -0
- package/scripts/hooks/opencode-hook-adapter.js +123 -0
- package/scripts/hooks/opencode-telemetry-hook.js +101 -0
- package/scripts/hooks/pi-hook-adapter.js +123 -0
- package/scripts/hooks/pi-telemetry-hook.js +105 -0
- package/scripts/hooks/run-hook.js +8 -0
- package/scripts/hooks/utterance-check.js +327 -0
- package/scripts/telemetry/lib/config.sh +5 -1
- package/skills/idea-to-backlog/SKILL.md +1 -1
- package/src/cli/init.ts +219 -6
- package/src/cli/utterance-check.ts +324 -0
- package/src/cli.ts +3 -0
- package/src/tools/build-universal-bundles.ts +266 -0
- package/src/tools/filter-installed-packs.ts +3 -0
- package/src/tools/validate-source-tree.ts +6 -1
- package/build/src/cli/docs-preview.js +0 -39
- package/build/src/cli/export-bookmarks.js +0 -38
- package/build/src/cli/import-bookmarks.js +0 -50
- package/build/src/cli/instinct-cli.js +0 -93
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
steering.py — Workflow-steering context loader.
|
|
3
|
+
|
|
4
|
+
Mirrors the logic in scripts/hooks/workflow-steering.js for reading
|
|
5
|
+
.flow-agents/*/state.json and producing a steering text blob.
|
|
6
|
+
|
|
7
|
+
Unlike the JS version this module does NOT inject into a prompt directly —
|
|
8
|
+
Strands' BeforeInvocationEvent does not expose a mutable system_prompt at
|
|
9
|
+
callback time. Instead, the caller is expected to call
|
|
10
|
+
FlowAgentsHooks.steering_context() at Agent construction and prepend the
|
|
11
|
+
result to the system prompt. See README.md § Limitations for details.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
# Active statuses that warrant surfacing steering context (mirrors workflow-steering.js)
|
|
22
|
+
_ACTIVE_STATUSES = frozenset(
|
|
23
|
+
[
|
|
24
|
+
"new",
|
|
25
|
+
"planning",
|
|
26
|
+
"planned",
|
|
27
|
+
"in_progress",
|
|
28
|
+
"blocked",
|
|
29
|
+
"verifying",
|
|
30
|
+
"verified",
|
|
31
|
+
"needs_decision",
|
|
32
|
+
"not_verified",
|
|
33
|
+
"failed",
|
|
34
|
+
"delivered",
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_AMBIENT_STATUSES = frozenset(["blocked", "failed", "needs_decision", "not_verified"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _find_repo_root(start: Optional[str] = None) -> Path:
|
|
42
|
+
"""Walk up from start until .git or AGENTS.md is found."""
|
|
43
|
+
current = Path(start).resolve() if start else Path.cwd()
|
|
44
|
+
for _ in range(40):
|
|
45
|
+
if (current / ".git").exists() or (current / "AGENTS.md").exists():
|
|
46
|
+
return current
|
|
47
|
+
parent = current.parent
|
|
48
|
+
if parent == current:
|
|
49
|
+
break
|
|
50
|
+
current = parent
|
|
51
|
+
return Path(start).resolve() if start else Path.cwd()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _walk_state_files(flow_agents_dir: Path) -> List[Path]:
|
|
55
|
+
"""Recursively find all state.json files, skipping archive/ dirs."""
|
|
56
|
+
results: List[Path] = []
|
|
57
|
+
if not flow_agents_dir.exists():
|
|
58
|
+
return results
|
|
59
|
+
for entry in flow_agents_dir.rglob("state.json"):
|
|
60
|
+
if "archive" in entry.parts:
|
|
61
|
+
continue
|
|
62
|
+
results.append(entry)
|
|
63
|
+
return results
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_json(path: Path) -> Optional[Dict[str, Any]]:
|
|
67
|
+
try:
|
|
68
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _safe_text(value: Any, max_length: int = 240) -> str:
|
|
74
|
+
text = " ".join(str(value or "").split()).strip()
|
|
75
|
+
if len(text) <= max_length:
|
|
76
|
+
return text
|
|
77
|
+
return text[: max_length - 3] + "..."
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SteeringContext:
|
|
81
|
+
"""
|
|
82
|
+
Loads Flow Agents workflow-steering context from .flow-agents/ state files.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, workspace: Optional[str] = None) -> None:
|
|
86
|
+
self._root = _find_repo_root(workspace)
|
|
87
|
+
self._flow_agents_dir = self._root / ".flow-agents"
|
|
88
|
+
|
|
89
|
+
def load(self) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Return a steering text string (possibly empty) for the current
|
|
92
|
+
workflow state. Mirrors the stateSteering() + contextMapSteering()
|
|
93
|
+
output from workflow-steering.js.
|
|
94
|
+
"""
|
|
95
|
+
parts: List[str] = []
|
|
96
|
+
|
|
97
|
+
state_hint = self._state_steering()
|
|
98
|
+
if state_hint:
|
|
99
|
+
parts.append(state_hint)
|
|
100
|
+
|
|
101
|
+
ctx_hint = self._context_map_steering()
|
|
102
|
+
if ctx_hint:
|
|
103
|
+
parts.append(ctx_hint)
|
|
104
|
+
|
|
105
|
+
if not parts:
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
return "\n\n---\n" + "\n".join(parts) + "\n---"
|
|
109
|
+
|
|
110
|
+
def _latest_active_state(self) -> Optional[Dict[str, Any]]:
|
|
111
|
+
candidates = []
|
|
112
|
+
for path in _walk_state_files(self._flow_agents_dir):
|
|
113
|
+
payload = _read_json(path)
|
|
114
|
+
if not payload:
|
|
115
|
+
continue
|
|
116
|
+
if payload.get("status") not in _ACTIVE_STATUSES:
|
|
117
|
+
continue
|
|
118
|
+
try:
|
|
119
|
+
mtime = path.stat().st_mtime_ns
|
|
120
|
+
except OSError:
|
|
121
|
+
continue
|
|
122
|
+
candidates.append((mtime, path, payload))
|
|
123
|
+
|
|
124
|
+
if not candidates:
|
|
125
|
+
return None
|
|
126
|
+
candidates.sort(key=lambda t: t[0], reverse=True)
|
|
127
|
+
_, path, payload = candidates[0]
|
|
128
|
+
return {"file": str(path), "payload": payload}
|
|
129
|
+
|
|
130
|
+
def _state_steering(self) -> str:
|
|
131
|
+
current = self._latest_active_state()
|
|
132
|
+
if not current:
|
|
133
|
+
return ""
|
|
134
|
+
state = current["payload"]
|
|
135
|
+
next_action = state.get("next_action") or {}
|
|
136
|
+
|
|
137
|
+
if next_action.get("status") == "done":
|
|
138
|
+
return ""
|
|
139
|
+
if state.get("status") in ("archived", "accepted"):
|
|
140
|
+
return ""
|
|
141
|
+
|
|
142
|
+
task_slug = state.get("task_slug") or Path(current["file"]).parent.name
|
|
143
|
+
parts = [
|
|
144
|
+
f"STATE: {task_slug} is status:{state.get('status')} phase:{state.get('phase')}."
|
|
145
|
+
]
|
|
146
|
+
if next_action.get("summary"):
|
|
147
|
+
parts.append(
|
|
148
|
+
f"Recorded next_action.summary: \"{_safe_text(next_action['summary'])}\""
|
|
149
|
+
)
|
|
150
|
+
if next_action.get("target_phase"):
|
|
151
|
+
parts.append(f"Target phase: {_safe_text(next_action['target_phase'], 80)}.")
|
|
152
|
+
if (
|
|
153
|
+
next_action.get("status") == "needs_user"
|
|
154
|
+
or state.get("status") in ("needs_decision", "not_verified")
|
|
155
|
+
):
|
|
156
|
+
parts.append(
|
|
157
|
+
"Do not deliver as complete until the user decision or accepted gap is recorded."
|
|
158
|
+
)
|
|
159
|
+
if state.get("status") == "failed":
|
|
160
|
+
parts.append("Route back through execution, then re-review and re-verify.")
|
|
161
|
+
|
|
162
|
+
return " ".join(parts)
|
|
163
|
+
|
|
164
|
+
def _context_map_steering(self) -> str:
|
|
165
|
+
map_path = self._root / "docs" / "context-map.md"
|
|
166
|
+
if not map_path.exists():
|
|
167
|
+
return ""
|
|
168
|
+
return (
|
|
169
|
+
"CONTEXT MAP: use docs/context-map.md before broad repo rediscovery. "
|
|
170
|
+
"If structure, commands, schemas, skills, agents, or packs changed, "
|
|
171
|
+
"run `npm run context-map -- --check`."
|
|
172
|
+
)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
telemetry.py — Canonical Flow Agents telemetry event builder and JSONL sink.
|
|
3
|
+
|
|
4
|
+
Event taxonomy mirrors the JS telemetry hooks exactly:
|
|
5
|
+
|
|
6
|
+
claude-telemetry-hook.js → canonicalEvent() mapping:
|
|
7
|
+
SessionStart → agentSpawn
|
|
8
|
+
UserPromptSubmit → userPromptSubmit
|
|
9
|
+
PreToolUse → preToolUse
|
|
10
|
+
PostToolUse → postToolUse
|
|
11
|
+
PostToolUseFailure → postToolUse
|
|
12
|
+
Stop / SessionEnd → stop
|
|
13
|
+
SubagentStart → subagentStart
|
|
14
|
+
SubagentStop → subagentStop
|
|
15
|
+
|
|
16
|
+
telemetry.sh → schema_event_type():
|
|
17
|
+
agentSpawn / SessionStart → session.start
|
|
18
|
+
stop / Stop / SessionEnd → session.end
|
|
19
|
+
userPromptSubmit / UserPromptSubmit → turn.user
|
|
20
|
+
preToolUse / PreToolUse → tool.invoke
|
|
21
|
+
postToolUse / PostToolUse → tool.result
|
|
22
|
+
|
|
23
|
+
Strands hook events are mapped to the same canonical names so the emitted
|
|
24
|
+
JSONL records are structurally identical to those produced by the Claude Code
|
|
25
|
+
and Codex telemetry hooks.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import time
|
|
33
|
+
import uuid
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any, Dict, Optional
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Strands → canonical event-name mapping
|
|
39
|
+
# (module-level dict so it is inspectable / documented)
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
STRANDS_TO_CANONICAL: Dict[str, str] = {
|
|
43
|
+
# Strands event class name → canonical Flow Agents event name
|
|
44
|
+
"AgentInitializedEvent": "agentSpawn",
|
|
45
|
+
"BeforeInvocationEvent": "userPromptSubmit",
|
|
46
|
+
"AfterInvocationEvent": "stop",
|
|
47
|
+
"BeforeToolCallEvent": "preToolUse",
|
|
48
|
+
"AfterToolCallEvent": "postToolUse",
|
|
49
|
+
"AfterModelCallEvent": "postToolUse", # closest analogue; no tool name
|
|
50
|
+
"MessageAddedEvent": "userPromptSubmit",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Canonical → schema event type (mirrors telemetry.sh schema_event_type())
|
|
54
|
+
_CANONICAL_TO_SCHEMA: Dict[str, str] = {
|
|
55
|
+
"agentSpawn": "session.start",
|
|
56
|
+
"userPromptSubmit": "turn.user",
|
|
57
|
+
"preToolUse": "tool.invoke",
|
|
58
|
+
"permissionRequest": "tool.permission_request",
|
|
59
|
+
"postToolUse": "tool.result",
|
|
60
|
+
"stop": "session.end",
|
|
61
|
+
"subagentStart": "agent.delegate",
|
|
62
|
+
"subagentStop": "agent.delegate",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _schema_event_type(canonical: str) -> str:
|
|
67
|
+
return _CANONICAL_TO_SCHEMA.get(canonical, "unknown")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# JSONL sink
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
class TelemetrySink:
|
|
75
|
+
"""
|
|
76
|
+
Writes canonical Flow Agents telemetry events to a JSONL file.
|
|
77
|
+
|
|
78
|
+
Default path: <workspace>/.flow-agents/.telemetry/full.jsonl
|
|
79
|
+
This matches the local-files sink convention from config.sh:
|
|
80
|
+
TELEMETRY_CHANNEL_FULL_LOG_FILE = <data_dir>/full.jsonl
|
|
81
|
+
where data_dir defaults to <repo_root>/.telemetry/
|
|
82
|
+
|
|
83
|
+
For the Strands adapter we follow the harness convention of writing
|
|
84
|
+
inside .flow-agents/.telemetry/ to keep everything under one dot-dir.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
DEFAULT_SUBDIR = Path(".flow-agents") / ".telemetry"
|
|
88
|
+
DEFAULT_FILENAME = "full.jsonl"
|
|
89
|
+
SCHEMA_VERSION = "0.3.0"
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
sink_path: Optional[str] = None,
|
|
94
|
+
workspace: Optional[str] = None,
|
|
95
|
+
agent_name: str = "strands-agent",
|
|
96
|
+
runtime: str = "strands",
|
|
97
|
+
) -> None:
|
|
98
|
+
self.agent_name = agent_name
|
|
99
|
+
self.runtime = runtime
|
|
100
|
+
self._session_id: Optional[str] = None
|
|
101
|
+
|
|
102
|
+
ws = Path(workspace) if workspace else Path.cwd()
|
|
103
|
+
if sink_path:
|
|
104
|
+
p = Path(sink_path)
|
|
105
|
+
# If given a directory, append default filename
|
|
106
|
+
if p.suffix == "":
|
|
107
|
+
self._log_file = p / self.DEFAULT_FILENAME
|
|
108
|
+
else:
|
|
109
|
+
self._log_file = p
|
|
110
|
+
else:
|
|
111
|
+
self._log_file = ws / self.DEFAULT_SUBDIR / self.DEFAULT_FILENAME
|
|
112
|
+
|
|
113
|
+
self._log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def session_id(self) -> str:
|
|
117
|
+
if self._session_id is None:
|
|
118
|
+
self._session_id = str(uuid.uuid4())
|
|
119
|
+
return self._session_id
|
|
120
|
+
|
|
121
|
+
def _base_event(self, schema_event_type: str) -> Dict[str, Any]:
|
|
122
|
+
"""Build the base event envelope matching telemetry.sh build_base_event()."""
|
|
123
|
+
return {
|
|
124
|
+
"schema_version": self.SCHEMA_VERSION,
|
|
125
|
+
"timestamp": str(int(time.time() * 1000)),
|
|
126
|
+
"session_id": self.session_id,
|
|
127
|
+
"event_id": str(uuid.uuid4()),
|
|
128
|
+
"event_type": schema_event_type,
|
|
129
|
+
"agent": {
|
|
130
|
+
"name": self.agent_name,
|
|
131
|
+
"runtime": self.runtime,
|
|
132
|
+
"version": "unknown",
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def emit(
|
|
137
|
+
self,
|
|
138
|
+
canonical_event: str,
|
|
139
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
140
|
+
) -> Dict[str, Any]:
|
|
141
|
+
"""
|
|
142
|
+
Build and write a canonical telemetry event.
|
|
143
|
+
|
|
144
|
+
Returns the emitted dict (useful for tests / callers that need the
|
|
145
|
+
event for further processing).
|
|
146
|
+
"""
|
|
147
|
+
schema_type = _schema_event_type(canonical_event)
|
|
148
|
+
event = self._base_event(schema_type)
|
|
149
|
+
|
|
150
|
+
# Attach hook context stub (mirrors add_hook_context() in telemetry.sh)
|
|
151
|
+
event["hook"] = {
|
|
152
|
+
"event_name": canonical_event,
|
|
153
|
+
"runtime_session_id": "",
|
|
154
|
+
"turn_id": "",
|
|
155
|
+
"transcript_path": "",
|
|
156
|
+
"model": "",
|
|
157
|
+
"source": "strands",
|
|
158
|
+
"stop_hook_active": None,
|
|
159
|
+
"last_assistant_message": "",
|
|
160
|
+
"raw_input": None,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if extra:
|
|
164
|
+
event.update(extra)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
with self._log_file.open("a", encoding="utf-8") as fh:
|
|
168
|
+
fh.write(json.dumps(event) + "\n")
|
|
169
|
+
except OSError:
|
|
170
|
+
pass # fail-open: telemetry must never block agent work
|
|
171
|
+
|
|
172
|
+
return event
|
|
173
|
+
|
|
174
|
+
def emit_session_start(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
175
|
+
return self.emit("agentSpawn", extra)
|
|
176
|
+
|
|
177
|
+
def emit_session_end(self, duration_s: float = 0.0) -> Dict[str, Any]:
|
|
178
|
+
return self.emit("stop", {"session": {"duration_s": duration_s}})
|
|
179
|
+
|
|
180
|
+
def emit_tool_invoke(
|
|
181
|
+
self,
|
|
182
|
+
tool_name: str,
|
|
183
|
+
tool_input: Optional[Dict[str, Any]] = None,
|
|
184
|
+
) -> Dict[str, Any]:
|
|
185
|
+
return self.emit(
|
|
186
|
+
"preToolUse",
|
|
187
|
+
{
|
|
188
|
+
"tool": {
|
|
189
|
+
"name": tool_name,
|
|
190
|
+
"normalized_name": _normalize_tool_name(tool_name),
|
|
191
|
+
"input": tool_input,
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def emit_tool_result(
|
|
197
|
+
self,
|
|
198
|
+
tool_name: str,
|
|
199
|
+
tool_output: Any = None,
|
|
200
|
+
) -> Dict[str, Any]:
|
|
201
|
+
return self.emit(
|
|
202
|
+
"postToolUse",
|
|
203
|
+
{
|
|
204
|
+
"tool": {
|
|
205
|
+
"name": tool_name,
|
|
206
|
+
"normalized_name": _normalize_tool_name(tool_name),
|
|
207
|
+
"output": tool_output,
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def emit_steering(self, steering_text: str) -> Dict[str, Any]:
|
|
213
|
+
"""Emit a synthetic userPromptSubmit event carrying steering context."""
|
|
214
|
+
return self.emit(
|
|
215
|
+
"userPromptSubmit",
|
|
216
|
+
{"turn": {"prompt_text": "", "steering_context": steering_text}},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _normalize_tool_name(name: str) -> str:
|
|
221
|
+
"""
|
|
222
|
+
Mirror telemetry.sh normalize_tool_name() for the most common cases.
|
|
223
|
+
"""
|
|
224
|
+
_MAP = {
|
|
225
|
+
"bash": "execute_bash",
|
|
226
|
+
"execute_bash": "execute_bash",
|
|
227
|
+
"shell": "execute_bash",
|
|
228
|
+
"edit": "fs_write",
|
|
229
|
+
"write": "fs_write",
|
|
230
|
+
"fs_write": "fs_write",
|
|
231
|
+
"apply_patch": "fs_write",
|
|
232
|
+
"read": "fs_read",
|
|
233
|
+
"fs_read": "fs_read",
|
|
234
|
+
"task": "use_subagent",
|
|
235
|
+
"agent": "use_subagent",
|
|
236
|
+
"use_subagent": "use_subagent",
|
|
237
|
+
}
|
|
238
|
+
return _MAP.get(name.lower(), name)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.backends.legacy:build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flow-agents-strands"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Flow Agents framework adapter for AWS Strands Agents — telemetry, policy gates, and workflow steering via the Strands hook surface."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["flow-agents", "strands", "aws", "agents", "telemetry", "hooks"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
# No runtime dependencies — strands-agents is optional
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# Install the real Strands SDK when you want to wire into a live Agent
|
|
28
|
+
strands = [
|
|
29
|
+
"strands-agents>=0.1.0",
|
|
30
|
+
]
|
|
31
|
+
# Development / test extras
|
|
32
|
+
dev = [
|
|
33
|
+
"strands-agents>=0.1.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["."]
|
|
38
|
+
include = ["flow_agents_strands*"]
|
|
File without changes
|