@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
|
@@ -57,7 +57,7 @@ fi
|
|
|
57
57
|
|
|
58
58
|
echo ""
|
|
59
59
|
echo "--- Bundle Layout ---"
|
|
60
|
-
for dir in "$DIST_DIR/kiro" "$DIST_DIR/claude-code" "$DIST_DIR/codex"; do
|
|
60
|
+
for dir in "$DIST_DIR/kiro" "$DIST_DIR/claude-code" "$DIST_DIR/codex" "$DIST_DIR/opencode" "$DIST_DIR/pi"; do
|
|
61
61
|
if [[ -d "$dir" ]]; then
|
|
62
62
|
_pass "$(basename "$dir") bundle exists"
|
|
63
63
|
else
|
|
@@ -80,6 +80,8 @@ codex_agents=$(find "$DIST_DIR/codex/.codex/agents" -maxdepth 1 -name '*.toml' 2
|
|
|
80
80
|
[[ "$kiro_agents" == "$source_agents" ]] && _pass "Kiro agent count matches source ($kiro_agents)" || _fail "Kiro agent count mismatch: source=$source_agents dist=$kiro_agents"
|
|
81
81
|
[[ "$claude_agents" == "$source_agents" ]] && _pass "Claude agent count matches source ($claude_agents)" || _fail "Claude agent count mismatch: source=$source_agents dist=$claude_agents"
|
|
82
82
|
[[ "$codex_agents" == "$expected_codex_agents" ]] && _pass "Codex agent count matches source minus manifest exclusions ($codex_agents)" || _fail "Codex agent count mismatch: expected=$expected_codex_agents dist=$codex_agents"
|
|
83
|
+
opencode_agents=$(find "$DIST_DIR/opencode/.opencode/agents" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
|
|
84
|
+
[[ "$opencode_agents" == "$source_agents" ]] && _pass "opencode agent count matches source ($opencode_agents)" || _fail "opencode agent count mismatch: source=$source_agents dist=$opencode_agents"
|
|
83
85
|
|
|
84
86
|
echo ""
|
|
85
87
|
echo "--- Kiro JSON ---"
|
|
@@ -226,9 +228,142 @@ else
|
|
|
226
228
|
_fail "Codex hooks missing telemetry/policy lifecycle coverage or CODEX_HOME root resolution"
|
|
227
229
|
fi
|
|
228
230
|
|
|
231
|
+
echo ""
|
|
232
|
+
echo "--- opencode Export Shape ---"
|
|
233
|
+
if node - "$DIST_DIR/opencode/.opencode/agents" <<'NODE'
|
|
234
|
+
const fs = require("node:fs");
|
|
235
|
+
const path = require("node:path");
|
|
236
|
+
const required = new Set(["description", "mode", "model"]);
|
|
237
|
+
const validModes = new Set(["subagent", "primary", "all"]);
|
|
238
|
+
for (const name of fs.readdirSync(process.argv[2]).filter((file) => file.endsWith(".md"))) {
|
|
239
|
+
const text = fs.readFileSync(path.join(process.argv[2], name), "utf8");
|
|
240
|
+
if (!text.startsWith("---\n")) throw new Error(`${name}: missing frontmatter start`);
|
|
241
|
+
const parts = text.split("\n---\n");
|
|
242
|
+
if (parts.length < 2) throw new Error(`${name}: missing frontmatter end`);
|
|
243
|
+
const fmLines = parts[0].replace("---\n", "").split(/\r?\n/).filter((line) => line.includes(":"));
|
|
244
|
+
const keys = new Set(fmLines.map((line) => line.split(":", 1)[0].trim()));
|
|
245
|
+
const missing = [...required].filter((key) => !keys.has(key));
|
|
246
|
+
if (missing.length) throw new Error(`${name}: missing frontmatter keys ${missing.join(", ")}`);
|
|
247
|
+
const modeMatch = fmLines.find((line) => line.trim().startsWith("mode:"));
|
|
248
|
+
if (modeMatch) {
|
|
249
|
+
const mode = modeMatch.split(":", 2)[1].trim();
|
|
250
|
+
if (!validModes.has(mode)) throw new Error(`${name}: invalid mode value: ${mode}`);
|
|
251
|
+
}
|
|
252
|
+
if (!parts.slice(1).join("\n---\n").trim()) throw new Error(`${name}: empty body`);
|
|
253
|
+
}
|
|
254
|
+
console.log("ok");
|
|
255
|
+
NODE
|
|
256
|
+
then
|
|
257
|
+
_pass "opencode agent markdown has valid YAML frontmatter with description, mode, model"
|
|
258
|
+
else
|
|
259
|
+
_fail "opencode agent markdown frontmatter/shape check failed"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
if [[ -f "$DIST_DIR/opencode/.opencode/plugins/flow-agents.js" ]]; then
|
|
263
|
+
_pass "opencode bundle includes Flow Agents plugin"
|
|
264
|
+
else
|
|
265
|
+
_fail "opencode bundle missing .opencode/plugins/flow-agents.js"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
if [[ -f "$DIST_DIR/opencode/opencode.json" ]]; then
|
|
269
|
+
if node - "$DIST_DIR/opencode/opencode.json" <<'NODE'
|
|
270
|
+
const fs = require("node:fs");
|
|
271
|
+
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
272
|
+
if (!data || typeof data !== "object") throw new Error("opencode.json must be an object");
|
|
273
|
+
// opencode's config schema rejects non-array `instructions` and aborts
|
|
274
|
+
// startup (caught by live acceptance smoke 2026-06-11). Pin the constraint.
|
|
275
|
+
if ("instructions" in data && !Array.isArray(data.instructions)) {
|
|
276
|
+
throw new Error("opencode.json instructions must be an array of file paths when present");
|
|
277
|
+
}
|
|
278
|
+
console.log("ok");
|
|
279
|
+
NODE
|
|
280
|
+
then
|
|
281
|
+
_pass "opencode.json is valid JSON and schema-safe (instructions array-or-absent)"
|
|
282
|
+
else
|
|
283
|
+
_fail "opencode.json is invalid or violates opencode config schema"
|
|
284
|
+
fi
|
|
285
|
+
else
|
|
286
|
+
_fail "opencode bundle missing opencode.json"
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# Generated hook artifacts must PARSE in their host language. The pi live
|
|
290
|
+
# smoke (2026-06-11) caught the generator emitting an unterminated string
|
|
291
|
+
# (template-literal escaping) that pi's loader rejected at startup.
|
|
292
|
+
if node --check "$DIST_DIR/opencode/.opencode/plugins/flow-agents.js" 2>/dev/null; then
|
|
293
|
+
_pass "generated opencode plugin parses as JavaScript"
|
|
294
|
+
else
|
|
295
|
+
_fail "generated opencode plugin has a JavaScript syntax error"
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# Semantic errors (TS2xxx: unresolved modules/types) are expected without the
|
|
299
|
+
# host's node_modules; only syntax-class errors (TS1xxx) mean a broken artifact.
|
|
300
|
+
PI_TS_SYNTAX_ERRORS=$(npx tsc --ignoreConfig --noEmit --noResolve --skipLibCheck --target esnext --module esnext \
|
|
301
|
+
"$DIST_DIR/pi/.pi/extensions/flow-agents.ts" 2>&1 | grep -c "error TS1" || true)
|
|
302
|
+
if [[ "$PI_TS_SYNTAX_ERRORS" -eq 0 ]]; then
|
|
303
|
+
_pass "generated pi extension parses as TypeScript (no TS1xxx syntax errors)"
|
|
304
|
+
else
|
|
305
|
+
_fail "generated pi extension has $PI_TS_SYNTAX_ERRORS TypeScript syntax errors"
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
if [[ -d "$DIST_DIR/opencode/.opencode/skills" ]] && [[ $(find "$DIST_DIR/opencode/.opencode/skills" -name "SKILL.md" | wc -l | tr -d ' ') -gt 0 ]]; then
|
|
309
|
+
_pass "opencode bundle includes skills in .opencode/skills/"
|
|
310
|
+
else
|
|
311
|
+
_fail "opencode bundle missing skills in .opencode/skills/"
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
echo ""
|
|
315
|
+
echo "--- pi Export Shape ---"
|
|
316
|
+
if [[ -f "$DIST_DIR/pi/.pi/extensions/flow-agents.ts" ]]; then
|
|
317
|
+
_pass "pi bundle includes Flow Agents extension"
|
|
318
|
+
else
|
|
319
|
+
_fail "pi bundle missing .pi/extensions/flow-agents.ts"
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
if [[ -d "$DIST_DIR/pi/.pi/skills" ]] && [[ $(find "$DIST_DIR/pi/.pi/skills" -name "SKILL.md" | wc -l | tr -d ' ') -gt 0 ]]; then
|
|
323
|
+
_pass "pi bundle includes skills in .pi/skills/"
|
|
324
|
+
else
|
|
325
|
+
_fail "pi bundle missing skills in .pi/skills/"
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
if node - "$DIST_DIR/pi/.pi/extensions/flow-agents.ts" <<'NODE'
|
|
329
|
+
const fs = require("node:fs");
|
|
330
|
+
const text = fs.readFileSync(process.argv[2], "utf8");
|
|
331
|
+
if (!text.includes("pi-hook-adapter.js")) throw new Error("pi extension does not reference pi-hook-adapter.js");
|
|
332
|
+
if (!text.includes("pi-telemetry-hook.js")) throw new Error("pi extension does not reference pi-telemetry-hook.js");
|
|
333
|
+
if (!text.includes("workflow-steering.js")) throw new Error("pi extension missing workflow-steering.js reference");
|
|
334
|
+
if (!text.includes("config-protection.js")) throw new Error("pi extension missing config-protection.js reference");
|
|
335
|
+
if (!text.includes("stop-goal-fit.js")) throw new Error("pi extension missing stop-goal-fit.js reference");
|
|
336
|
+
if (!text.includes("before_agent_start")) throw new Error("pi extension missing before_agent_start event handler");
|
|
337
|
+
if (!text.includes("tool_call")) throw new Error("pi extension missing tool_call event handler");
|
|
338
|
+
console.log("ok");
|
|
339
|
+
NODE
|
|
340
|
+
then
|
|
341
|
+
_pass "pi extension references correct hook adapters and event handlers"
|
|
342
|
+
else
|
|
343
|
+
_fail "pi extension is missing required hook adapter or event handler references"
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
if node - "$DIST_DIR/opencode/.opencode/plugins/flow-agents.js" <<'NODE'
|
|
347
|
+
const fs = require("node:fs");
|
|
348
|
+
const text = fs.readFileSync(process.argv[2], "utf8");
|
|
349
|
+
if (!text.includes("opencode-hook-adapter.js")) throw new Error("opencode plugin does not reference opencode-hook-adapter.js");
|
|
350
|
+
if (!text.includes("opencode-telemetry-hook.js")) throw new Error("opencode plugin does not reference opencode-telemetry-hook.js");
|
|
351
|
+
if (!text.includes("workflow-steering.js")) throw new Error("opencode plugin missing workflow-steering.js reference");
|
|
352
|
+
if (!text.includes("config-protection.js")) throw new Error("opencode plugin missing config-protection.js reference");
|
|
353
|
+
if (!text.includes("stop-goal-fit.js")) throw new Error("opencode plugin missing stop-goal-fit.js reference");
|
|
354
|
+
if (!text.includes("session.created")) throw new Error("opencode plugin missing session.created event handler");
|
|
355
|
+
if (!text.includes("tool.execute.before")) throw new Error("opencode plugin missing tool.execute.before event handler");
|
|
356
|
+
console.log("ok");
|
|
357
|
+
NODE
|
|
358
|
+
then
|
|
359
|
+
_pass "opencode plugin references correct hook adapters and event handlers"
|
|
360
|
+
else
|
|
361
|
+
_fail "opencode plugin is missing required hook adapter or event handler references"
|
|
362
|
+
fi
|
|
363
|
+
|
|
229
364
|
echo ""
|
|
230
365
|
echo "--- Shared Task Dirs ---"
|
|
231
|
-
for dir in "$DIST_DIR/claude-code/.flow-agents" "$DIST_DIR/codex/.flow-agents"; do
|
|
366
|
+
for dir in "$DIST_DIR/claude-code/.flow-agents" "$DIST_DIR/codex/.flow-agents" "$DIST_DIR/opencode/.flow-agents" "$DIST_DIR/pi/.flow-agents"; do
|
|
232
367
|
if [[ -d "$dir" ]]; then
|
|
233
368
|
_pass "$(realpath "$dir" 2>/dev/null || echo "$dir") exists"
|
|
234
369
|
else
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# flow-agents-strands
|
|
2
|
+
|
|
3
|
+
**SPIKE — Framework Adapter Proof of Concept**
|
|
4
|
+
|
|
5
|
+
This package proves the thesis: Flow Agents' process-discipline layer
|
|
6
|
+
(telemetry events + workflow steering + policy gates) can compile to a
|
|
7
|
+
**framework adapter** hook surface — here, AWS Strands Agents — not just
|
|
8
|
+
coding-agent harnesses.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Harness adapters vs. framework adapters
|
|
13
|
+
|
|
14
|
+
The existing Flow Agents adapters (`claude-code/`, `codex/`, `kiro/`) are
|
|
15
|
+
**harness adapters**: they integrate with coding-agent runtimes that each have
|
|
16
|
+
their own hook format (JSON on stdin, exit codes, lifecycle events named by
|
|
17
|
+
the harness). Each adapter normalizes its harness's hook payloads into the
|
|
18
|
+
canonical Flow Agents telemetry taxonomy and then delegates to the shared
|
|
19
|
+
`scripts/telemetry/telemetry.sh` sink.
|
|
20
|
+
|
|
21
|
+
This package is a **framework adapter**: Strands Agents is not a coding-agent
|
|
22
|
+
harness — it is a general-purpose Python agent SDK. Its hook surface
|
|
23
|
+
(`HookProvider` / `HookRegistry`) is class-based and synchronous rather than
|
|
24
|
+
process-based. This means:
|
|
25
|
+
|
|
26
|
+
- No stdin/stdout protocol.
|
|
27
|
+
- No process exit codes as block signals.
|
|
28
|
+
- Hook callbacks receive typed Python event objects and can mutate them in
|
|
29
|
+
place (e.g. set `event.cancel_tool` to block a tool call).
|
|
30
|
+
|
|
31
|
+
Despite these surface differences, the **same canonical event taxonomy** is
|
|
32
|
+
used. The JSONL output from `FlowAgentsHooks` is structurally identical to
|
|
33
|
+
the output produced by `claude-telemetry-hook.js` and `codex-telemetry-hook.js`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Canonical event taxonomy
|
|
38
|
+
|
|
39
|
+
All telemetry events follow the schema defined in `scripts/telemetry/telemetry.sh`.
|
|
40
|
+
The Strands → canonical mapping is exposed as a module-level dict:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from flow_agents_strands import STRANDS_TO_CANONICAL
|
|
44
|
+
# {
|
|
45
|
+
# "AgentInitializedEvent": "agentSpawn",
|
|
46
|
+
# "BeforeInvocationEvent": "userPromptSubmit",
|
|
47
|
+
# "AfterInvocationEvent": "stop",
|
|
48
|
+
# "BeforeToolCallEvent": "preToolUse",
|
|
49
|
+
# "AfterToolCallEvent": "postToolUse",
|
|
50
|
+
# "AfterModelCallEvent": "postToolUse",
|
|
51
|
+
# "MessageAddedEvent": "userPromptSubmit",
|
|
52
|
+
# }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Canonical names map to schema `event_type` values:
|
|
56
|
+
|
|
57
|
+
| Canonical name | Schema event_type |
|
|
58
|
+
|-----------------------|----------------------------|
|
|
59
|
+
| `agentSpawn` | `session.start` |
|
|
60
|
+
| `userPromptSubmit` | `turn.user` |
|
|
61
|
+
| `preToolUse` | `tool.invoke` |
|
|
62
|
+
| `permissionRequest` | `tool.permission_request` |
|
|
63
|
+
| `postToolUse` | `tool.result` |
|
|
64
|
+
| `stop` | `session.end` |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Telemetry sink
|
|
69
|
+
|
|
70
|
+
Events are written to `.flow-agents/.telemetry/full.jsonl` by default,
|
|
71
|
+
matching the local-files sink convention in `scripts/telemetry/lib/config.sh`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
TELEMETRY_CHANNEL_FULL_LOG_FILE = <data_dir>/full.jsonl
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The JSON record shape matches `build_base_event()` in `telemetry.sh`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"schema_version": "0.3.0",
|
|
82
|
+
"timestamp": "1718000000000",
|
|
83
|
+
"session_id": "<uuid>",
|
|
84
|
+
"event_id": "<uuid>",
|
|
85
|
+
"event_type": "tool.invoke",
|
|
86
|
+
"agent": { "name": "strands-agent", "runtime": "strands", "version": "unknown" },
|
|
87
|
+
"hook": { "event_name": "preToolUse", "source": "strands", ... },
|
|
88
|
+
"tool": { "name": "edit", "normalized_name": "fs_write", "input": {...} }
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Policy gates
|
|
95
|
+
|
|
96
|
+
The config-protection policy binds to the canonical Node.js engine
|
|
97
|
+
(`scripts/hooks/run-hook.js → config-protection.js`) via subprocess, consuming
|
|
98
|
+
the engine contract (contract_version "1.0") rather than reimplementing it.
|
|
99
|
+
|
|
100
|
+
**How it works:**
|
|
101
|
+
|
|
102
|
+
1. On `BeforeToolCallEvent`, if the tool name is a write-like tool, the gate
|
|
103
|
+
serialises the event to the canonical JSON payload and spawns:
|
|
104
|
+
`node run-hook.js config-protection config-protection.js`
|
|
105
|
+
2. The engine exits 2 (block) or 0 (allow). Exit code 2 causes `event.cancel_tool`
|
|
106
|
+
to be set to the block reason from stderr.
|
|
107
|
+
3. All other exit codes fail-open (do not block the agent).
|
|
108
|
+
|
|
109
|
+
**Fallback when Node.js is unavailable:**
|
|
110
|
+
|
|
111
|
+
If `node` is not on PATH or the engine script (`run-hook.js`) cannot be located,
|
|
112
|
+
the gate degrades to a built-in Python implementation of the same logic and emits
|
|
113
|
+
a `RuntimeWarning`. The pure-Python implementation mirrors `PROTECTED_FILES` from
|
|
114
|
+
`config-protection.js` exactly. See §Limitations for the degradation contract.
|
|
115
|
+
|
|
116
|
+
**Custom protected_files:** if you pass a custom `frozenset` to `PolicyGate`,
|
|
117
|
+
Python evaluation is used directly (the engine subprocess cannot receive a
|
|
118
|
+
runtime-custom set). This is intended for tests and local override only.
|
|
119
|
+
|
|
120
|
+
On `BeforeToolCallEvent`, if the tool name is a write-like tool and the target
|
|
121
|
+
file is in the protected set, `event.cancel_tool` is set to the block reason.
|
|
122
|
+
Strands will cancel the call and surface the message as the tool result.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Workflow steering
|
|
127
|
+
|
|
128
|
+
The JS `workflow-steering.js` hook injects steering text by appending it to
|
|
129
|
+
the prompt payload. Strands' `BeforeInvocationEvent` does **not** expose a
|
|
130
|
+
mutable system prompt at callback time.
|
|
131
|
+
|
|
132
|
+
**Spike approach:** call `hooks.steering_context()` at Agent construction and
|
|
133
|
+
append the result to the system prompt:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
hooks = FlowAgentsHooks(workspace=".")
|
|
137
|
+
system_prompt = "You are a helpful agent.\n" + hooks.steering_context()
|
|
138
|
+
agent = Agent(system_prompt=system_prompt, hooks=[hooks])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`steering_context()` also emits a `turn.user` telemetry event so the steering
|
|
142
|
+
injection is recorded in the JSONL log.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Quickstart
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from strands import Agent
|
|
150
|
+
from strands.models import BedrockModel
|
|
151
|
+
from flow_agents_strands import FlowAgentsHooks
|
|
152
|
+
|
|
153
|
+
# Build hooks — no strands import needed for this step
|
|
154
|
+
hooks = FlowAgentsHooks(
|
|
155
|
+
workspace=".", # root of your project (reads .flow-agents/)
|
|
156
|
+
agent_name="my-agent", # appears in telemetry events
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Load steering context BEFORE constructing the agent
|
|
160
|
+
system_prompt = (
|
|
161
|
+
"You are a helpful assistant.\n"
|
|
162
|
+
+ hooks.steering_context() # appends workflow state reminders if any
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Wire hooks into the Agent
|
|
166
|
+
model = BedrockModel(model_id="anthropic.claude-3-5-sonnet-20241022-v2:0")
|
|
167
|
+
agent = Agent(model=model, system_prompt=system_prompt, hooks=[hooks])
|
|
168
|
+
|
|
169
|
+
result = agent("List the files in this directory.")
|
|
170
|
+
print(result)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Telemetry is written to `.flow-agents/.telemetry/full.jsonl`.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Installation
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Without strands (for telemetry/policy use only, tests, etc.):
|
|
181
|
+
pip install flow-agents-strands
|
|
182
|
+
|
|
183
|
+
# With strands SDK:
|
|
184
|
+
pip install "flow-agents-strands[strands]"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Running tests
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
cd integrations/strands
|
|
193
|
+
python3 -m unittest discover
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Tests use stdlib `unittest` only — no pytest, no strands-agents required.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Limitations (honest spike notes)
|
|
201
|
+
|
|
202
|
+
1. **Node.js subprocess dependency**: The primary policy binding spawns a Node.js
|
|
203
|
+
subprocess for each `BeforeToolCallEvent` that involves a write-like tool. If
|
|
204
|
+
`node` is not on PATH or the `@kontourai/flow-agents` package is not installed,
|
|
205
|
+
the gate degrades gracefully to the built-in Python fallback with a one-time
|
|
206
|
+
`RuntimeWarning`. The Python fallback uses the same `PROTECTED_FILES` constant
|
|
207
|
+
as the engine and is auditable. To force the subprocess path, set
|
|
208
|
+
`FLOW_AGENTS_ENGINE_PATH` to the absolute path of `run-hook.js`.
|
|
209
|
+
|
|
210
|
+
2. **Steering seam**: Strands does not allow mutating the system prompt from
|
|
211
|
+
`BeforeInvocationEvent`. The workaround (`steering_context()` at Agent
|
|
212
|
+
construction) is a one-shot snapshot; it does not re-evaluate on every turn
|
|
213
|
+
the way the JS hook does at `UserPromptSubmit`. Productization would
|
|
214
|
+
require either a custom Strands model wrapper that injects context per-turn,
|
|
215
|
+
or upstream SDK support for mutable system-prompt context in the invocation
|
|
216
|
+
event.
|
|
217
|
+
|
|
218
|
+
3. **session.usage event omitted**: The JS harness emits a `session.usage`
|
|
219
|
+
event on stop with token counts pulled from the transcript. The Strands
|
|
220
|
+
`AfterInvocationEvent` does not (yet) expose token-usage data in the hook
|
|
221
|
+
payload, so this event is not emitted. Productization would need to read
|
|
222
|
+
usage from the agent's response object and attach it here.
|
|
223
|
+
|
|
224
|
+
4. **No analytics channel**: The harness adapters write to two channels
|
|
225
|
+
(full + analytics) with different redaction profiles. This spike writes
|
|
226
|
+
only to the `full` channel. Adding analytics is straightforward: a second
|
|
227
|
+
`TelemetrySink` instance pointed at `analytics.jsonl` with the analytics
|
|
228
|
+
redact list applied.
|
|
229
|
+
|
|
230
|
+
5. **No Console/HTTP sink**: The bash transport supports POSTing events to a
|
|
231
|
+
Console endpoint. This adapter writes JSONL only. Adding HTTP transport
|
|
232
|
+
would mean replicating the `console_telemetry_emit()` logic in Python or
|
|
233
|
+
calling `transport.sh` as a subprocess.
|
|
234
|
+
|
|
235
|
+
6. **Runtime version is "unknown"**: The harness adapters run
|
|
236
|
+
`<runtime> --version` to populate `agent.version`. Strands does not
|
|
237
|
+
expose its version through the hook event; `importlib.metadata` could
|
|
238
|
+
provide the SDK version as a proxy.
|
|
239
|
+
|
|
240
|
+
7. **No subagent / delegation event**: The Strands SDK does not have a
|
|
241
|
+
built-in InvokeSubagents tool; the delegation telemetry path is not wired.
|
|
242
|
+
|
|
243
|
+
8. **Quality-gate policy omitted**: `quality-gate.js` invokes ruff/biome
|
|
244
|
+
after edits. This is omitted from the spike because it requires executing
|
|
245
|
+
external formatters and has no clear Strands analogue yet.
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
## What productization would require
|
|
249
|
+
|
|
250
|
+
- Upstream Strands SDK support for mutable per-turn context injection.
|
|
251
|
+
- Token-usage exposure in `AfterInvocationEvent` for `session.usage` events.
|
|
252
|
+
- Dual-channel JSONL + optional HTTP transport mirroring the bash transport.
|
|
253
|
+
- Packaging as a proper release with semantic versioning once the Strands hook
|
|
254
|
+
API stabilizes.
|
|
255
|
+
- Integration tests against a live Strands agent (currently blocked by missing
|
|
256
|
+
AWS credentials in CI).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
example.py — Intended usage of FlowAgentsHooks with a real Strands Agent.
|
|
4
|
+
|
|
5
|
+
Guarded by try/except ImportError so it degrades gracefully when
|
|
6
|
+
strands-agents is not installed (e.g. in CI or unit-test environments).
|
|
7
|
+
|
|
8
|
+
Run this only when:
|
|
9
|
+
1. strands-agents is installed: pip install "flow-agents-strands[strands]"
|
|
10
|
+
2. AWS credentials are configured for Bedrock (or swap to a different model)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from flow_agents_strands import FlowAgentsHooks
|
|
14
|
+
|
|
15
|
+
# Step 1: Build hooks — no strands import required
|
|
16
|
+
hooks = FlowAgentsHooks(
|
|
17
|
+
workspace=".", # root of your project (reads .flow-agents/)
|
|
18
|
+
agent_name="example-agent",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Step 2: Load steering context at agent construction time.
|
|
22
|
+
# This is the documented spike workaround for the system-prompt seam:
|
|
23
|
+
# BeforeInvocationEvent does not expose a mutable system_prompt in Strands,
|
|
24
|
+
# so we snapshot workflow state once and prepend it to the system prompt.
|
|
25
|
+
# See README.md § Limitations for details.
|
|
26
|
+
base_system_prompt = (
|
|
27
|
+
"You are a helpful assistant. "
|
|
28
|
+
"Follow the Flow Agents workflow discipline."
|
|
29
|
+
)
|
|
30
|
+
steering = hooks.steering_context()
|
|
31
|
+
system_prompt = base_system_prompt + steering
|
|
32
|
+
|
|
33
|
+
print("=== Flow Agents Strands Example ===")
|
|
34
|
+
print(f"Steering context ({len(steering)} chars):")
|
|
35
|
+
print(steering or "(none — no active workflow state found)")
|
|
36
|
+
print()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from strands import Agent # type: ignore[import]
|
|
40
|
+
from strands.models import BedrockModel # type: ignore[import]
|
|
41
|
+
|
|
42
|
+
model = BedrockModel(
|
|
43
|
+
model_id="anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
44
|
+
region_name="us-east-1",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
agent = Agent(
|
|
48
|
+
model=model,
|
|
49
|
+
system_prompt=system_prompt,
|
|
50
|
+
hooks=[hooks],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
print("Agent created. Running a simple task...")
|
|
54
|
+
result = agent("List the Python files in the current directory.")
|
|
55
|
+
print("Result:", result)
|
|
56
|
+
print()
|
|
57
|
+
print("Telemetry written to .flow-agents/.telemetry/full.jsonl")
|
|
58
|
+
|
|
59
|
+
except ImportError:
|
|
60
|
+
print(
|
|
61
|
+
"strands-agents is not installed.\n"
|
|
62
|
+
"Install it to run against a live agent:\n"
|
|
63
|
+
" pip install 'flow-agents-strands[strands]'\n"
|
|
64
|
+
"\n"
|
|
65
|
+
"The hooks and telemetry modules work without strands-agents installed.\n"
|
|
66
|
+
"Run the unit tests with:\n"
|
|
67
|
+
" python3 -m unittest discover"
|
|
68
|
+
)
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
print(f"Agent run failed (likely missing AWS credentials): {exc}")
|
|
71
|
+
print(
|
|
72
|
+
"\nThis example requires AWS credentials configured for Bedrock.\n"
|
|
73
|
+
"The hooks + telemetry code ran successfully up to the Agent() call."
|
|
74
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flow_agents_strands — Flow Agents framework adapter for AWS Strands Agents.
|
|
3
|
+
|
|
4
|
+
Provides FlowAgentsHooks, a HookProvider (duck-typed so strands-agents is
|
|
5
|
+
optional at import time) that wires Flow Agents' canonical telemetry events,
|
|
6
|
+
policy gates, and workflow-steering context into the Strands hook surface.
|
|
7
|
+
|
|
8
|
+
Importable without strands-agents installed:
|
|
9
|
+
|
|
10
|
+
from flow_agents_strands import FlowAgentsHooks
|
|
11
|
+
hooks = FlowAgentsHooks() # no strands needed yet
|
|
12
|
+
ctx = hooks.steering_context() # load steering context anywhere
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .hooks import FlowAgentsHooks
|
|
16
|
+
from .telemetry import STRANDS_TO_CANONICAL, TelemetrySink
|
|
17
|
+
from .policy import PolicyGate
|
|
18
|
+
from .steering import SteeringContext
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"FlowAgentsHooks",
|
|
22
|
+
"STRANDS_TO_CANONICAL",
|
|
23
|
+
"TelemetrySink",
|
|
24
|
+
"PolicyGate",
|
|
25
|
+
"SteeringContext",
|
|
26
|
+
]
|
|
27
|
+
__version__ = "0.0.1"
|