@kontourai/flow-agents 0.2.0 → 0.4.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/workflows/release-please.yml +13 -1
- package/.github/workflows/runtime-compat.yml +1 -1
- package/AGENTS.md +8 -1
- package/CHANGELOG.md +41 -0
- package/README.md +38 -19
- package/build/src/cli/flow-kit.js +9 -4
- package/build/src/cli/runtime-adapter.js +9 -5
- package/build/src/cli/telemetry-doctor.js +4 -1
- package/build/src/runtime-adapters.js +34 -0
- package/build/src/tools/build-universal-bundles.js +18 -1
- package/console.telemetry.json +115 -20
- package/docs/_layouts/default.html +2 -0
- package/docs/index.md +8 -0
- package/docs/integrations/index.md +4 -0
- package/docs/integrations/knowledge-kit-live.md +211 -0
- package/docs/kit-authoring-guide.md +169 -0
- package/docs/spec/runtime-hook-surface.md +56 -3
- package/evals/acceptance/run.sh +10 -1
- package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
- package/evals/acceptance/test_pi_harness.sh +15 -0
- package/evals/integration/test_runtime_adapter_activation.sh +113 -1
- package/evals/static/test_universal_bundles.sh +10 -0
- package/integrations/strands/examples/knowledge_kit_live.py +461 -0
- package/integrations/strands/flow_agents_strands/steering.py +54 -1
- package/integrations/strands/tests/test_hooks.py +88 -0
- package/integrations/strands-ts/src/hooks.ts +104 -0
- package/integrations/strands-ts/test/test-steering.ts +159 -0
- package/kits/catalog.json +6 -0
- package/kits/knowledge/adapters/default-store/index.js +902 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1469 -0
- package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
- package/kits/knowledge/adapters/similarity-vector/index.js +284 -0
- package/kits/knowledge/docs/README.md +328 -0
- package/kits/knowledge/docs/store-contract.md +650 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +675 -0
- package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1173 -0
- package/kits/knowledge/evals/similarity-vector/suite.test.js +685 -0
- package/kits/knowledge/evals/synthesis/suite.test.js +916 -0
- package/kits/knowledge/flows/compile.flow.json +60 -0
- package/kits/knowledge/flows/consolidate.flow.json +77 -0
- package/kits/knowledge/flows/ingest.flow.json +60 -0
- package/kits/knowledge/flows/retire.flow.json +77 -0
- package/kits/knowledge/flows/store-contract.flow.json +48 -0
- package/kits/knowledge/flows/synthesize.flow.json +77 -0
- package/kits/knowledge/kit.json +98 -0
- package/package.json +1 -1
- package/src/cli/flow-kit.ts +10 -4
- package/src/cli/runtime-adapter.ts +10 -5
- package/src/cli/telemetry-doctor.ts +4 -1
- package/src/runtime-adapters.ts +35 -0
- package/src/tools/build-universal-bundles.ts +18 -1
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* // All methods callable; registerHooks() is a no-op without the SDK.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
29
31
|
import { TelemetrySink } from "./telemetry.js";
|
|
30
32
|
import { PolicyGate } from "./policy.js";
|
|
31
33
|
|
|
@@ -58,6 +60,81 @@ export interface StrandsEvent {
|
|
|
58
60
|
[key: string]: unknown;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Kit flow discovery (Issue #32, Decision Q3: option (a))
|
|
65
|
+
//
|
|
66
|
+
// activateStrandsLocal (src/runtime-adapters.ts) writes kit flow files to
|
|
67
|
+
// .flow-agents/runtime/strands/flows/<kit-id>/<asset-id>.flow.json.
|
|
68
|
+
// steeringContext() reads those files and surfaces their id + description
|
|
69
|
+
// so the agent is aware of available workflow guidance without the hooks
|
|
70
|
+
// needing to know anything about the catalog layout.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
interface KitFlowEntry {
|
|
74
|
+
kitId: string;
|
|
75
|
+
assetId: string;
|
|
76
|
+
description: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findRepoRoot(start: string): string {
|
|
80
|
+
let current = path.resolve(start);
|
|
81
|
+
for (let i = 0; i < 40; i++) {
|
|
82
|
+
if (
|
|
83
|
+
fs.existsSync(path.join(current, ".git")) ||
|
|
84
|
+
fs.existsSync(path.join(current, "AGENTS.md"))
|
|
85
|
+
) {
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
const parent = path.dirname(current);
|
|
89
|
+
if (parent === current) break;
|
|
90
|
+
current = parent;
|
|
91
|
+
}
|
|
92
|
+
return path.resolve(start);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readKitFlows(flowAgentsDir: string): KitFlowEntry[] {
|
|
96
|
+
const flowsDir = path.join(flowAgentsDir, "runtime", "strands", "flows");
|
|
97
|
+
if (!fs.existsSync(flowsDir)) return [];
|
|
98
|
+
const results: KitFlowEntry[] = [];
|
|
99
|
+
|
|
100
|
+
function walkDir(dir: string): void {
|
|
101
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
102
|
+
const full = path.join(dir, name);
|
|
103
|
+
const stat = fs.statSync(full);
|
|
104
|
+
if (stat.isDirectory()) {
|
|
105
|
+
walkDir(full);
|
|
106
|
+
} else if (name.endsWith(".flow.json")) {
|
|
107
|
+
try {
|
|
108
|
+
const payload = JSON.parse(fs.readFileSync(full, "utf8")) as Record<string, unknown>;
|
|
109
|
+
const stemName = name.replace(/\.flow\.json$/, "");
|
|
110
|
+
const assetId = typeof payload.id === "string" ? payload.id : stemName;
|
|
111
|
+
const description = typeof payload.description === "string" ? payload.description : "";
|
|
112
|
+
// kit_id is the directory component between flows/ and the file
|
|
113
|
+
const rel = path.relative(flowsDir, full);
|
|
114
|
+
const relParts = rel.split(path.sep);
|
|
115
|
+
const kitId = relParts.length >= 2 ? relParts[0] : "";
|
|
116
|
+
results.push({ kitId, assetId, description });
|
|
117
|
+
} catch {
|
|
118
|
+
// Malformed file — skip silently (fail-open)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
walkDir(flowsDir);
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildKitFlowsHint(flows: KitFlowEntry[]): string {
|
|
129
|
+
if (flows.length === 0) return "";
|
|
130
|
+
const lines = ["KIT FLOWS: the following kit flows are activated for this workspace:"];
|
|
131
|
+
for (const flow of flows) {
|
|
132
|
+
const desc = flow.description ? ` — ${flow.description.slice(0, 120)}` : "";
|
|
133
|
+
lines.push(` • ${flow.assetId}${desc}`);
|
|
134
|
+
}
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
61
138
|
// ---------------------------------------------------------------------------
|
|
62
139
|
// FlowAgentsHooks options
|
|
63
140
|
// ---------------------------------------------------------------------------
|
|
@@ -85,9 +162,12 @@ export interface FlowAgentsHooksOptions {
|
|
|
85
162
|
export class FlowAgentsHooks {
|
|
86
163
|
private readonly sink: TelemetrySink;
|
|
87
164
|
private readonly policyGate: PolicyGate;
|
|
165
|
+
private readonly _workspace: string;
|
|
88
166
|
private _sessionStartMs: number | null = null;
|
|
89
167
|
|
|
90
168
|
constructor(options: FlowAgentsHooksOptions = {}) {
|
|
169
|
+
this._workspace = findRepoRoot(options.workspace ?? process.cwd());
|
|
170
|
+
|
|
91
171
|
this.sink = new TelemetrySink({
|
|
92
172
|
sinkPath: options.sinkPath,
|
|
93
173
|
workspace: options.workspace,
|
|
@@ -100,6 +180,30 @@ export class FlowAgentsHooks {
|
|
|
100
180
|
});
|
|
101
181
|
}
|
|
102
182
|
|
|
183
|
+
// --------------------------------------------------------------------------
|
|
184
|
+
// Steering context — available without strands-agents installed (Issue #32 AC2)
|
|
185
|
+
// --------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Return workflow-steering context text for the current workspace.
|
|
189
|
+
*
|
|
190
|
+
* Includes activated kit flows discovered from the strands-local runtime
|
|
191
|
+
* path (.flow-agents/runtime/strands/flows/) written by
|
|
192
|
+
* `flow-kit activate --adapter strands-local`.
|
|
193
|
+
*
|
|
194
|
+
* Callers should prepend this to the Agent's system prompt:
|
|
195
|
+
*
|
|
196
|
+
* const hooks = new FlowAgentsHooks({ workspace: "." });
|
|
197
|
+
* const agent = new Agent({ systemPrompt: basePrompt + hooks.steeringContext() });
|
|
198
|
+
*/
|
|
199
|
+
steeringContext(): string {
|
|
200
|
+
const flowAgentsDir = path.join(this._workspace, ".flow-agents");
|
|
201
|
+
const flows = readKitFlows(flowAgentsDir);
|
|
202
|
+
const kitFlowsHint = buildKitFlowsHint(flows);
|
|
203
|
+
if (!kitFlowsHint) return "";
|
|
204
|
+
return "\n\n---\n" + kitFlowsHint + "\n---";
|
|
205
|
+
}
|
|
206
|
+
|
|
103
207
|
// --------------------------------------------------------------------------
|
|
104
208
|
// HookProvider protocol — registerHooks(registry)
|
|
105
209
|
// This is the sole method required by the Strands HookProvider protocol.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-steering.ts — Tests for FlowAgentsHooks.steeringContext() kit flow surfacing.
|
|
3
|
+
*
|
|
4
|
+
* Issue #32 AC2: steering context surfaces activated kit flows from the
|
|
5
|
+
* strands-local runtime path (.flow-agents/runtime/strands/flows/).
|
|
6
|
+
*
|
|
7
|
+
* Fixture approach: write fake *.flow.json files (same structure as the real
|
|
8
|
+
* kit flow files produced by activateStrandsLocal) and assert the steering
|
|
9
|
+
* context text contains kit flow ids and descriptions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { test, describe } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { FlowAgentsHooks } from "../src/hooks.js";
|
|
18
|
+
|
|
19
|
+
function makeTmpDir(): string {
|
|
20
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "fa-ts-steering-"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeFlow(
|
|
24
|
+
workspace: string,
|
|
25
|
+
kitId: string,
|
|
26
|
+
assetId: string,
|
|
27
|
+
description = ""
|
|
28
|
+
): void {
|
|
29
|
+
const flowsDir = path.join(
|
|
30
|
+
workspace,
|
|
31
|
+
".flow-agents",
|
|
32
|
+
"runtime",
|
|
33
|
+
"strands",
|
|
34
|
+
"flows",
|
|
35
|
+
kitId
|
|
36
|
+
);
|
|
37
|
+
fs.mkdirSync(flowsDir, { recursive: true });
|
|
38
|
+
const safeName = assetId.replace(/\./g, "-");
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.join(flowsDir, `${safeName}.flow.json`),
|
|
41
|
+
JSON.stringify({ id: assetId, description }),
|
|
42
|
+
"utf8"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("FlowAgentsHooks.steeringContext() — kit flow surfacing (Issue #32 AC2)", () => {
|
|
47
|
+
test("empty string when no runtime strands dir exists", () => {
|
|
48
|
+
const tmpDir = makeTmpDir();
|
|
49
|
+
try {
|
|
50
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
51
|
+
const ctx = hooks.steeringContext();
|
|
52
|
+
assert.strictEqual(ctx, "", "Expected empty steering context with no runtime dir");
|
|
53
|
+
} finally {
|
|
54
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("KIT FLOWS hint appears when a flow file exists", () => {
|
|
59
|
+
const tmpDir = makeTmpDir();
|
|
60
|
+
try {
|
|
61
|
+
writeFlow(tmpDir, "builder", "builder.shape", "Shape a problem.");
|
|
62
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
63
|
+
const ctx = hooks.steeringContext();
|
|
64
|
+
assert.ok(ctx.includes("KIT FLOWS"), `Expected 'KIT FLOWS' in context, got: ${ctx}`);
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("flow asset_id appears in steering context", () => {
|
|
71
|
+
const tmpDir = makeTmpDir();
|
|
72
|
+
try {
|
|
73
|
+
writeFlow(tmpDir, "builder", "builder.shape", "Shape a problem.");
|
|
74
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
75
|
+
const ctx = hooks.steeringContext();
|
|
76
|
+
assert.ok(
|
|
77
|
+
ctx.includes("builder.shape"),
|
|
78
|
+
`Expected 'builder.shape' in context, got: ${ctx}`
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("flow description appears in steering context", () => {
|
|
86
|
+
const tmpDir = makeTmpDir();
|
|
87
|
+
try {
|
|
88
|
+
writeFlow(tmpDir, "builder", "builder.build", "Build a feature end-to-end.");
|
|
89
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
90
|
+
const ctx = hooks.steeringContext();
|
|
91
|
+
assert.ok(
|
|
92
|
+
ctx.includes("Build a feature end-to-end."),
|
|
93
|
+
`Expected description in context, got: ${ctx}`
|
|
94
|
+
);
|
|
95
|
+
} finally {
|
|
96
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("multiple flows all listed in steering context", () => {
|
|
101
|
+
const tmpDir = makeTmpDir();
|
|
102
|
+
try {
|
|
103
|
+
writeFlow(tmpDir, "builder", "builder.shape", "Shape.");
|
|
104
|
+
writeFlow(tmpDir, "builder", "builder.build", "Build.");
|
|
105
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
106
|
+
const ctx = hooks.steeringContext();
|
|
107
|
+
assert.ok(ctx.includes("builder.shape"), "Expected builder.shape in context");
|
|
108
|
+
assert.ok(ctx.includes("builder.build"), "Expected builder.build in context");
|
|
109
|
+
} finally {
|
|
110
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("malformed flow JSON does not crash; other flows still listed", () => {
|
|
115
|
+
const tmpDir = makeTmpDir();
|
|
116
|
+
try {
|
|
117
|
+
writeFlow(tmpDir, "builder", "builder.shape", "Shape.");
|
|
118
|
+
// Write a malformed flow file
|
|
119
|
+
const flowsDir = path.join(
|
|
120
|
+
tmpDir,
|
|
121
|
+
".flow-agents",
|
|
122
|
+
"runtime",
|
|
123
|
+
"strands",
|
|
124
|
+
"flows",
|
|
125
|
+
"builder"
|
|
126
|
+
);
|
|
127
|
+
fs.writeFileSync(path.join(flowsDir, "bad.flow.json"), "{ not valid json", "utf8");
|
|
128
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
129
|
+
const ctx = hooks.steeringContext();
|
|
130
|
+
// builder.shape should still appear
|
|
131
|
+
assert.ok(ctx.includes("builder.shape"), "Expected builder.shape despite bad file");
|
|
132
|
+
} finally {
|
|
133
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("steering context wraps with --- delimiters", () => {
|
|
138
|
+
const tmpDir = makeTmpDir();
|
|
139
|
+
try {
|
|
140
|
+
writeFlow(tmpDir, "builder", "builder.shape", "Shape.");
|
|
141
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
142
|
+
const ctx = hooks.steeringContext();
|
|
143
|
+
assert.ok(ctx.includes("---"), "Expected --- delimiters in context");
|
|
144
|
+
} finally {
|
|
145
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns string type (even empty)", () => {
|
|
150
|
+
const tmpDir = makeTmpDir();
|
|
151
|
+
try {
|
|
152
|
+
const hooks = new FlowAgentsHooks({ workspace: tmpDir });
|
|
153
|
+
const ctx = hooks.steeringContext();
|
|
154
|
+
assert.strictEqual(typeof ctx, "string");
|
|
155
|
+
} finally {
|
|
156
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
package/kits/catalog.json
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
"name": "Builder Kit",
|
|
7
7
|
"path": "kits/builder",
|
|
8
8
|
"description": "Flow-backed shaping, planning, build, verification, merge readiness, pull request readiness, and learning workflows."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "knowledge",
|
|
12
|
+
"name": "Knowledge Kit",
|
|
13
|
+
"path": "kits/knowledge",
|
|
14
|
+
"description": "Store contract with record types (raw/compiled/concept), mutation operations with required provenance, default markdown+frontmatter+wikilink+graph-index adapter, and a parameterized contract test suite."
|
|
9
15
|
}
|
|
10
16
|
]
|
|
11
17
|
}
|