@kontourai/flow-agents 0.2.0 → 0.3.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.
Files changed (46) hide show
  1. package/.github/workflows/runtime-compat.yml +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/README.md +38 -19
  4. package/build/src/cli/flow-kit.js +9 -4
  5. package/build/src/cli/runtime-adapter.js +9 -5
  6. package/build/src/cli/telemetry-doctor.js +4 -1
  7. package/build/src/runtime-adapters.js +34 -0
  8. package/build/src/tools/build-universal-bundles.js +18 -1
  9. package/console.telemetry.json +115 -20
  10. package/docs/_layouts/default.html +2 -0
  11. package/docs/index.md +8 -0
  12. package/docs/integrations/index.md +4 -0
  13. package/docs/integrations/knowledge-kit-live.md +211 -0
  14. package/docs/kit-authoring-guide.md +169 -0
  15. package/docs/spec/runtime-hook-surface.md +56 -3
  16. package/evals/acceptance/run.sh +10 -1
  17. package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
  18. package/evals/acceptance/test_pi_harness.sh +15 -0
  19. package/evals/integration/test_runtime_adapter_activation.sh +113 -1
  20. package/integrations/strands/examples/knowledge_kit_live.py +461 -0
  21. package/integrations/strands/flow_agents_strands/steering.py +54 -1
  22. package/integrations/strands/tests/test_hooks.py +88 -0
  23. package/integrations/strands-ts/src/hooks.ts +104 -0
  24. package/integrations/strands-ts/test/test-steering.ts +159 -0
  25. package/kits/catalog.json +6 -0
  26. package/kits/knowledge/adapters/default-store/index.js +821 -0
  27. package/kits/knowledge/adapters/flow-runner/index.js +1179 -0
  28. package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
  29. package/kits/knowledge/docs/README.md +135 -0
  30. package/kits/knowledge/docs/store-contract.md +526 -0
  31. package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
  32. package/kits/knowledge/evals/contract-suite/suite.test.js +670 -0
  33. package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
  34. package/kits/knowledge/evals/synthesis/suite.test.js +909 -0
  35. package/kits/knowledge/flows/compile.flow.json +60 -0
  36. package/kits/knowledge/flows/consolidate.flow.json +77 -0
  37. package/kits/knowledge/flows/ingest.flow.json +60 -0
  38. package/kits/knowledge/flows/store-contract.flow.json +48 -0
  39. package/kits/knowledge/flows/synthesize.flow.json +77 -0
  40. package/kits/knowledge/kit.json +78 -0
  41. package/package.json +1 -1
  42. package/src/cli/flow-kit.ts +10 -4
  43. package/src/cli/runtime-adapter.ts +10 -5
  44. package/src/cli/telemetry-doctor.ts +4 -1
  45. package/src/runtime-adapters.ts +35 -0
  46. package/src/tools/build-universal-bundles.ts +18 -1
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env bash
2
+ # test_knowledge_kit_live.sh — Acceptance: Knowledge Kit S5 live example
3
+ #
4
+ # Gated on:
5
+ # 1. ollama binary at /run/current-system/sw/bin/ollama
6
+ # 2. qwen3:1.7b model pulled (checked via ollama list)
7
+ # 3. Python venv with strands-agents[ollama] at /tmp/strands-py-live/venv
8
+ #
9
+ # Skips cleanly if any gate is absent (matching other harness conventions).
10
+ # Starts ollama serve, runs the live example, asserts evidence, stops ollama.
11
+ #
12
+ # Assertions:
13
+ # A1. Script exits 0 (overall PASS printed)
14
+ # A2. <workspace>/.telemetry/full.jsonl exists and contains tool.invoke + tool.result
15
+ # A3. <workspace>/.flow-agents/.telemetry/full.jsonl exists and contains
16
+ # session.start, tool.invoke, tool.result (FlowAgentsHooks events)
17
+ # A4. No new .telemetry directory created in the workspace's parent directory
18
+ # by this script (pre-existing parent-dir .telemetry is not counted)
19
+ # A5. At least 1 compiled record in <workspace>/.knowledge-store/records/
20
+ # A6. Compiled record has provenance source_ids referencing raw records
21
+ #
22
+ set -uo pipefail
23
+
24
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
25
+
26
+ OLLAMA_BIN="/run/current-system/sw/bin/ollama"
27
+ VENV_PYTHON="/tmp/strands-py-live/venv/bin/python3"
28
+ EXAMPLE_SCRIPT="$ROOT_DIR/integrations/strands/examples/knowledge_kit_live.py"
29
+
30
+ pass=0
31
+ fail=0
32
+ skip=0
33
+ OLLAMA_STARTED=0
34
+
35
+ _pass() { echo " ✓ $1"; pass=$((pass + 1)); }
36
+ _fail() { echo " ✗ $1"; fail=$((fail + 1)); }
37
+ _skip() { echo " ○ $1"; skip=$((skip + 1)); }
38
+
39
+ cleanup() {
40
+ if [[ "$OLLAMA_STARTED" -eq 1 ]]; then
41
+ pkill -f "ollama serve" 2>/dev/null || true
42
+ fi
43
+ }
44
+ trap cleanup EXIT
45
+
46
+ echo "=== Acceptance: Knowledge Kit S5 Live Example ==="
47
+ echo ""
48
+
49
+ # ── Gate checks ─────────────────────────────────────────────────────────────
50
+ if [[ ! -x "$OLLAMA_BIN" ]]; then
51
+ _skip "ollama binary not found at $OLLAMA_BIN"
52
+ echo ""
53
+ echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
54
+ exit 0
55
+ fi
56
+
57
+ if [[ ! -x "$VENV_PYTHON" ]]; then
58
+ _skip "Python venv not found at $VENV_PYTHON — run: python3 -m venv /tmp/strands-py-live/venv && /tmp/strands-py-live/venv/bin/pip install 'strands-agents[ollama]'"
59
+ echo ""
60
+ echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
61
+ exit 0
62
+ fi
63
+
64
+ _pass "Gate: ollama binary present"
65
+ _pass "Gate: Python venv with strands-agents present"
66
+ echo ""
67
+
68
+ # ── Start ollama serve ───────────────────────────────────────────────────────
69
+ echo "--- Starting ollama serve ---"
70
+ "$OLLAMA_BIN" serve > /tmp/ollama-knowledge-kit-live.log 2>&1 &
71
+ OLLAMA_STARTED=1
72
+
73
+ # Wait for server to be ready (up to 15 seconds)
74
+ for i in {1..15}; do
75
+ if curl -s localhost:11434/v1/models >/dev/null 2>&1; then
76
+ _pass "ollama serve ready (${i}s)"
77
+ break
78
+ fi
79
+ if [[ "$i" -eq 15 ]]; then
80
+ _fail "ollama serve did not start within 15 seconds"
81
+ echo ""
82
+ echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
83
+ exit 1
84
+ fi
85
+ sleep 1
86
+ done
87
+
88
+ # Model gate AFTER server start: ollama list errors when no server is running,
89
+ # which previously misreported a pulled model as missing (skip-path bug).
90
+ if ! "$OLLAMA_BIN" list 2>/dev/null | grep -q "qwen3:1.7b"; then
91
+ _skip "qwen3:1.7b model not pulled — run: ollama pull qwen3:1.7b"
92
+ echo ""
93
+ echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
94
+ exit 0
95
+ fi
96
+ _pass "Gate: qwen3:1.7b model pulled"
97
+ echo ""
98
+
99
+ # ── Run the example ──────────────────────────────────────────────────────────
100
+ echo "--- Running knowledge_kit_live.py ---"
101
+ EXAMPLE_OUTPUT="$(mktemp /tmp/knowledge-kit-live-output.XXXXXX)"
102
+
103
+ FLOW_AGENTS_ROOT="$ROOT_DIR" \
104
+ "$VENV_PYTHON" "$EXAMPLE_SCRIPT" 2>&1 | tee "$EXAMPLE_OUTPUT"
105
+ EXAMPLE_EXIT="${PIPESTATUS[0]}"
106
+
107
+ echo ""
108
+
109
+ # ── Assert A1: script exits 0 ─────────────────────────────────────────────
110
+ if [[ "$EXAMPLE_EXIT" -eq 0 ]]; then
111
+ _pass "A1: example script exits 0"
112
+ else
113
+ _fail "A1: example script exited $EXAMPLE_EXIT"
114
+ fi
115
+
116
+ # Extract workspace path from script output
117
+ WORKSPACE="$(grep "^Workspace: " "$EXAMPLE_OUTPUT" | head -1 | sed 's/^Workspace: //')"
118
+ if [[ -z "$WORKSPACE" ]]; then
119
+ _fail "Could not extract workspace path from script output"
120
+ echo ""
121
+ echo "Results: ${pass}/$((pass + fail)) passed, ${fail} failed, ${skip} skipped"
122
+ exit 1
123
+ fi
124
+
125
+ echo " Workspace: $WORKSPACE"
126
+ KIT_TELEMETRY="$WORKSPACE/.telemetry/full.jsonl"
127
+ SESSION_TELEMETRY="$WORKSPACE/.flow-agents/.telemetry/full.jsonl"
128
+ STORE_RECORDS="$WORKSPACE/.knowledge-store/records"
129
+
130
+ # ── Assert A2: kit telemetry contains tool.invoke + tool.result ───────────
131
+ if [[ -f "$KIT_TELEMETRY" ]] && \
132
+ node -e "
133
+ const fs = require('fs');
134
+ const lines = fs.readFileSync('$KIT_TELEMETRY', 'utf8').trim().split('\n').filter(Boolean);
135
+ const types = lines.map(l => { try { return JSON.parse(l).event_type; } catch(e) { return ''; } });
136
+ const required = ['tool.invoke', 'tool.result'];
137
+ const missing = required.filter(t => !types.includes(t));
138
+ if (missing.length > 0) { process.stderr.write('missing: ' + missing.join(', ') + '\n'); process.exit(1); }
139
+ " 2>/dev/null; then
140
+ _pass "A2: kit telemetry contains tool.invoke + tool.result gate events"
141
+ else
142
+ _fail "A2: kit telemetry missing or lacks required event types (tool.invoke, tool.result)"
143
+ fi
144
+
145
+ # ── Assert A3: session telemetry contains session.start, tool.invoke, tool.result ─
146
+ if [[ -f "$SESSION_TELEMETRY" ]] && \
147
+ node -e "
148
+ const fs = require('fs');
149
+ const lines = fs.readFileSync('$SESSION_TELEMETRY', 'utf8').trim().split('\n').filter(Boolean);
150
+ const types = lines.map(l => { try { return JSON.parse(l).event_type; } catch(e) { return ''; } });
151
+ const required = ['session.start', 'tool.invoke', 'tool.result'];
152
+ const missing = required.filter(t => !types.includes(t));
153
+ if (missing.length > 0) { process.stderr.write('missing: ' + missing.join(', ') + '\n'); process.exit(1); }
154
+ " 2>/dev/null; then
155
+ _pass "A3: session telemetry contains session.start, tool.invoke, tool.result"
156
+ else
157
+ _fail "A3: session telemetry missing or lacks required FlowAgentsHooks events"
158
+ fi
159
+
160
+ # ── Assert A4: workspace telemetry does not leak to parent ────────────────
161
+ # This assertion checks that telemetry written during this test run does not
162
+ # appear in the parent directory. We verify that the workspace telemetry is
163
+ # contained within WORKSPACE, not in its parent.
164
+ # (Pre-existing .telemetry in the system temp dir is not counted as a leak.)
165
+ PARENT_TELEMETRY="$(dirname "$WORKSPACE")/.telemetry"
166
+ if [[ -d "$PARENT_TELEMETRY" ]]; then
167
+ # Only fail if the directory was modified during our test (mtime within last 60s)
168
+ PARENT_MTIME="$(find "$PARENT_TELEMETRY" -newer "$EXAMPLE_OUTPUT" -name "*.jsonl" 2>/dev/null | wc -l | tr -d ' ')"
169
+ if [[ "$PARENT_MTIME" -gt 0 ]]; then
170
+ _fail "A4: telemetry leaked — new .jsonl files written to workspace parent directory during this test"
171
+ else
172
+ _pass "A4: workspace telemetry contained within workspace (pre-existing parent .telemetry not modified by this test)"
173
+ fi
174
+ else
175
+ _pass "A4: no .telemetry in workspace parent directory"
176
+ fi
177
+
178
+ # ── Assert A5: at least 1 compiled record exists ─────────────────────────
179
+ COMPILED_COUNT=0
180
+ if [[ -d "$STORE_RECORDS" ]]; then
181
+ COMPILED_COUNT=$(grep -rl "type: compiled" "$STORE_RECORDS"/*.md 2>/dev/null | wc -l | tr -d ' ')
182
+ fi
183
+ if [[ "$COMPILED_COUNT" -ge 1 ]]; then
184
+ _pass "A5: compiled record found in store ($COMPILED_COUNT)"
185
+ else
186
+ _fail "A5: no compiled records found in $STORE_RECORDS"
187
+ fi
188
+
189
+ # ── Assert A6: compiled record has provenance source_ids ─────────────────
190
+ PROVENANCE_OK=0
191
+ if [[ -d "$STORE_RECORDS" ]]; then
192
+ for compiled_md in "$STORE_RECORDS"/*.md; do
193
+ [[ -f "$compiled_md" ]] || continue
194
+ if grep -q "type: compiled" "$compiled_md" && grep -q "source_ids:" "$compiled_md"; then
195
+ # Verify at least 2 raw ids are referenced
196
+ SOURCE_COUNT=$(grep -c "^ - " "$compiled_md" 2>/dev/null || echo 0)
197
+ if [[ "$SOURCE_COUNT" -ge 2 ]]; then
198
+ PROVENANCE_OK=1
199
+ break
200
+ fi
201
+ fi
202
+ done
203
+ fi
204
+ if [[ "$PROVENANCE_OK" -eq 1 ]]; then
205
+ _pass "A6: compiled record has provenance source_ids with resolving raw refs"
206
+ else
207
+ _fail "A6: compiled record missing source_ids or insufficient provenance refs"
208
+ fi
209
+
210
+ # ── Cleanup temp files ───────────────────────────────────────────────────
211
+ rm -f "$EXAMPLE_OUTPUT"
212
+ if [[ -d "$WORKSPACE" ]]; then
213
+ rm -rf "$WORKSPACE"
214
+ fi
215
+
216
+ echo ""
217
+ echo "==========================="
218
+ total=$((pass + fail))
219
+ echo "Results: ${pass}/${total} passed, ${fail} failed, ${skip} skipped"
220
+ [[ "$fail" -gt 0 ]] && exit 1
221
+ exit 0
@@ -81,6 +81,21 @@ process.exit(0);
81
81
  else
82
82
  _fail "pi telemetry missing one or more required event types (session.start, tool.invoke, tool.result, session.end)"
83
83
  fi
84
+
85
+ # Assert session.start appears exactly once (guards against before_agent_start double-emit).
86
+ if [[ -f "$telemetry_file" ]] && \
87
+ node -e "
88
+ const fs = require('fs');
89
+ const lines = fs.readFileSync('$telemetry_file', 'utf8').trim().split('\n');
90
+ const types = lines.map(l => { try { return JSON.parse(l).event_type; } catch(e) { return ''; } });
91
+ const count = types.filter(t => t === 'session.start').length;
92
+ if (count !== 1) { process.stderr.write('session.start count=' + count + ' (expected exactly 1)\n'); process.exit(1); }
93
+ process.exit(0);
94
+ " 2>/dev/null; then
95
+ _pass "pi telemetry: session.start appears exactly once (no double-emit)"
96
+ else
97
+ _fail "pi telemetry: session.start count is not 1 (double-emit or missing)"
98
+ fi
84
99
  fi
85
100
 
86
101
  PARENT_TELEMETRY="$(dirname "$TMP_WORK")/.telemetry"
@@ -86,16 +86,128 @@ elif node - "$UNKNOWN_OUT" <<'NODE'
86
86
  const fs = require("node:fs");
87
87
  const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
88
88
  if (!data.available_adapters?.includes("codex-local")) throw new Error("available adapters missing codex-local");
89
+ if (!data.available_adapters?.includes("strands-local")) throw new Error("available adapters missing strands-local");
89
90
  if (!data.errors?.length) throw new Error("unknown adapter did not report errors");
90
91
  console.log("ok");
91
92
  NODE
92
93
  then
93
- pass "unknown adapter reports available adapters"
94
+ pass "unknown adapter reports available adapters (codex-local and strands-local)"
94
95
  else
95
96
  fail "unknown adapter diagnostics missing"
96
97
  sed -n '1,120p' "$UNKNOWN_OUT"
97
98
  fi
98
99
 
100
+ # -------------------------------------------------------------------------
101
+ # strands-local adapter activation (Issue #32 AC1)
102
+ # -------------------------------------------------------------------------
103
+
104
+ echo ""
105
+ echo "=== strands-local Adapter Activation Checks (Issue #32 AC1) ==="
106
+
107
+ STRANDS_DEST="$TMP_DIR/strands-dest"
108
+ STRANDS_OUT="$TMP_DIR/strands-activation.json"
109
+ mkdir -p "$STRANDS_DEST"
110
+
111
+ # Use the builder kit (stable fixture) — activate for strands-local from the repo source root
112
+ if flow_agents_node "$CLI" activate --dest "$STRANDS_DEST" --source-root "$ROOT" --adapter strands-local --format json >"$STRANDS_OUT" 2>&1; then
113
+ pass "strands-local activation succeeds"
114
+ else
115
+ fail "strands-local activation failed"
116
+ sed -n '1,220p' "$STRANDS_OUT"
117
+ fi
118
+
119
+ if node - "$STRANDS_OUT" "$STRANDS_DEST" "$ROOT/kits/catalog.json" <<'NODE'
120
+ const fs = require("node:fs");
121
+ const path = require("node:path");
122
+ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
123
+ const dest = process.argv[3];
124
+ const catalog = process.argv[4];
125
+
126
+ // Verify selected_adapter
127
+ if (data.selected_adapter !== "strands-local") throw new Error(`expected strands-local, got: ${data.selected_adapter}`);
128
+ if (JSON.stringify(data.supported_asset_classes) !== JSON.stringify(["flows"])) throw new Error(`unexpected supported_asset_classes: ${JSON.stringify(data.supported_asset_classes)}`);
129
+
130
+ // Verify builder kit flows are generated (builder kit is in catalog.json)
131
+ const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
132
+ for (const expected of ["builder.shape", "builder.build", "strands-local.activation"]) {
133
+ if (!ids.has(expected)) throw new Error(`missing generated asset: ${expected}`);
134
+ }
135
+
136
+ // Verify generated runtime files actually exist on disk
137
+ for (const item of data.generated_runtime_files) {
138
+ if (item.asset_class === "activation-manifest") continue;
139
+ const generatedPath = path.join(dest, item.path);
140
+ if (!fs.existsSync(generatedPath)) throw new Error(`generated file missing: ${generatedPath}`);
141
+ // Verify runtime files are under .flow-agents/runtime/strands/flows/
142
+ if (!item.path.includes(".flow-agents/runtime/strands/flows/")) {
143
+ throw new Error(`generated path not under strands runtime dir: ${item.path}`);
144
+ }
145
+ }
146
+
147
+ // Verify activation.json written at strands runtime dir
148
+ const manifestPath = path.join(dest, ".flow-agents/runtime/strands/activation.json");
149
+ if (!fs.existsSync(manifestPath)) throw new Error("strands runtime activation.json missing");
150
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
151
+ if (manifest.adapter !== "strands-local") throw new Error(`activation.json adapter mismatch: ${manifest.adapter}`);
152
+ if (!Array.isArray(manifest.skipped_assets)) throw new Error("activation.json missing skipped_assets array");
153
+
154
+ // Verify skipped_assets have expected fields (parity with codex-local)
155
+ for (const item of manifest.skipped_assets) {
156
+ for (const key of ["asset_class", "path", "kit_id", "asset_id", "reason"]) {
157
+ if (!(key in item)) throw new Error(`skipped asset missing ${key}: ${JSON.stringify(item)}`);
158
+ }
159
+ if (!item.reason.includes("diagnostic-only")) throw new Error(`unexpected skip reason: ${item.reason}`);
160
+ }
161
+
162
+ // Non-flow asset classes should appear in skipped_assets
163
+ const skippedClasses = new Set(manifest.skipped_assets.map((item) => item.asset_class));
164
+ // builder kit has flows only; skipped_assets check requires a kit with non-flow assets,
165
+ // which the codex-local path already validates via mixed-runtime-kit above.
166
+ // Here we just confirm the field structure is present.
167
+ if (!Array.isArray(data.skipped_assets)) throw new Error("result skipped_assets is not an array");
168
+
169
+ // Catalog not mutated
170
+ if (path.resolve(catalog) === path.resolve(path.join(dest, ".flow-agents/runtime/strands/activation.json"))) {
171
+ throw new Error("activation generated over kits/catalog.json");
172
+ }
173
+
174
+ console.log("ok");
175
+ NODE
176
+ then
177
+ pass "strands-local: runtime flow files, activation.json, and skipped_assets present with correct structure"
178
+ else
179
+ fail "strands-local: activation diagnostics incomplete or incorrect"
180
+ sed -n '1,220p' "$STRANDS_OUT"
181
+ fi
182
+
183
+ # Verify codex-local activation is still intact (AC3 — existing tests still pass)
184
+ if flow_agents_node "$CLI" activate --dest "$STRANDS_DEST" --source-root "$ROOT" --format json >"$TMP_DIR/codex-after-strands.json" 2>&1; then
185
+ pass "codex-local still activates after strands-local has run"
186
+ else
187
+ fail "codex-local activation failed after strands-local activation"
188
+ sed -n '1,220p' "$TMP_DIR/codex-after-strands.json"
189
+ fi
190
+
191
+ if node - "$TMP_DIR/codex-after-strands.json" "$STRANDS_DEST" <<'NODE'
192
+ const fs = require("node:fs");
193
+ const path = require("node:path");
194
+ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
195
+ const dest = process.argv[3];
196
+ if (data.selected_adapter !== "codex-local") throw new Error(`expected codex-local, got: ${data.selected_adapter}`);
197
+ const manifestPath = path.join(dest, ".flow-agents/runtime/codex/activation.json");
198
+ if (!fs.existsSync(manifestPath)) throw new Error("codex activation.json still not present");
199
+ // Strands runtime dir must also still exist
200
+ const strandsManifestPath = path.join(dest, ".flow-agents/runtime/strands/activation.json");
201
+ if (!fs.existsSync(strandsManifestPath)) throw new Error("strands activation.json was removed by codex-local run");
202
+ console.log("ok");
203
+ NODE
204
+ then
205
+ pass "codex-local and strands-local runtime dirs co-exist independently (AC3)"
206
+ else
207
+ fail "co-existence check failed"
208
+ sed -n '1,220p' "$TMP_DIR/codex-after-strands.json"
209
+ fi
210
+
99
211
  echo ""
100
212
  if [[ "$errors" -eq 0 ]]; then
101
213
  echo "Runtime adapter activation checks passed."