Skip to content

feat: add AgenticComponentActivity for agent-authored bundle components - #96

Open
matthewmoorcroft wants to merge 47 commits into
stack/agentic-insightsfrom
stack/agentic-component
Open

matthewmoorcroft wants to merge 47 commits into
stack/agentic-insightsfrom
stack/agentic-component

Conversation

@matthewmoorcroft

@matthewmoorcroft matthewmoorcroft commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Intent

Add AgenticComponentActivity, the in-engine escape hatch for a source activity the typed engine can't express. The agent authors raw bundle files, Lakeflow pipeline resources, job environments and a Databricks task fragment, and package writes them into the bundle as given. flowx still owns the task's identity, its dependencies and its run policy (D4): those always come from the activity and win over anything the agent wrote.

What changed

  • models/ir.py, ir_serde.py, dab_writer._reconstruct_ir: the new IR type (files, resources, environments, task, raw_definition). It round-trips through the serialized report.
  • Merging agentic results (ir_serde._find_and_replace_task): when an agentic component replaces a placeholder, the placeholder's identity and run policy always win, including the ADF timeout and retries. These are task_key, name, depends_on, timeout_seconds, max_retries and min_retry_interval_millis. The component also keeps the placeholder's source metadata (notifications, compute_mode, description, data_reads, data_writes, raw_definition and the like) for each field the agent leaves out; a value the agent sets wins. NotebookActivity merging is unchanged.
  • preparer/activity_preparers/agentic_component.py:
    • Written as given: the fragment goes into the bundle unchanged. flowx sets only FLOWX_OWNED_TASK_FIELDS and rejects a fragment that sets any of them.
    • Named wiring passes: a few passes that make the bundle work apply to authored tasks just as they do to generated ones. They are listed in the docstring and in skills/flowx-convert/sources/adf.md.
    • Plumbing: where a value is absent, flowx adds only the plumbing the task needs (the ForEach item, collapsed notifications, a cluster for a notebook that needs one). It never overrides a value the fragment set.
    • Refusals: package refuses any of these:
      • a fragment without exactly one *_task payload, or that sets a flowx-owned field, at top level or in a for_each_task body (the body keeps its own task_key);
      • a for_each_task without a body task carrying a task_key;
      • a Python, wheel, JAR or dbt task that names no compute, or a spark_submit_task without new_cluster or a job_cluster_key;
      • a compute key set to null or empty, or more than one compute key on one task;
      • a job_cluster_key that isn't one of the clusters flowx defines;
      • a notebook_task.notebook_path that isn't a non-empty string, or base_parameters / job_parameters that aren't mappings;
      • libraries or existing_cluster_id set on the component instead of in the fragment;
      • a file path that escapes src, including Windows drive-only or root-only paths, or that uses backslashes;
      • malformed files, resources or environments, including a file without exactly one of content or binary_content;
      • unknown fields on a file entry, a resource entry or the component itself;
      • binary content that isn't valid base64.
  • bundler/dab_writer.py:
    • Collision checks: packaging stops before writing any file when an authored file shares its path with a different file below src (another component's, a generated notebook or a setup notebook). It also stops when two authored paths differ only in letter case or one file path is another file's directory, and when a pipeline resource or environment key is reused with a different definition, or a pipeline key clashes with a job key. Identical duplicates are written once.
    • Environments: they are written at job level for the jobs whose tasks reference them, and every environment_key an authored task names must be defined.
    • Authored files: a plain DabNotebook.authored flag keeps them under src, and their text is written byte for byte, with no added newline and no platform line endings.
  • preparer/activity_preparers/{if_condition,switch,for_each}.py: forward their children's pipeline resources and environments, so a component nested in control flow keeps them.
  • validate/bundle_invariants.py: reports dangling_pipeline_reference when a pipeline_task points at a pipeline the bundle doesn't declare.
  • Carries feat: add enrich step that records agent-authored insights on the discover inventory #95's final head 6e29e8a (including feat(adf): save source graphs with dataset lineage at ADF discover #94 and refactor: add source-neutral discovery graph contract and shared lineage core #84) through clean --no-ff merges, with no conflicts. Nothing in feat: add AgenticComponentActivity for agent-authored bundle components #96 refers to agentic_insights.json, authored_against or the inventory rebuild, so they needed no changes. uv.lock is identical to feat: add enrich step that records agent-authored insights on the discover inventory #95's.

How it was tested

  • Unit tests: tests/unit/test_agentic_component.py and test_bundle_invariants.py cover:

    • the round trip, and packaging serverless spark_python_task and python_wheel_task components with their environments;
    • every refusal and collision rule;
    • D4 through the real merge path;
    • each wiring and plumbing site;
    • components nested in control flow.

    The full unit suite (2005 passed, 8 skipped), ruff and mypy pass.

  • Deterministic output: ADF discover, convert and package on the fixture pipelines produce bundles, metadata and translation reports byte-identical to feat: add enrich step that records agent-authored insights on the discover inventory #95's head 6e29e8a (325 files).

  • End to end: the no-mistakes test step drove the component through the real CLI (discover, convert, merge-agentic, package). The bundle validated with 0 errors against the DAB JSON schema.

  • Codex cross-review, read-only, three times. Every fix has a regression test that fails on the previous code.

    • Round 1 found three feat: add AgenticComponentActivity for agent-authored bundle components #96 issues, all fixed in 9e70c54, with 63d12ad from the no-mistakes review:

      1. Two authored files at one path with different content: the second was renamed, but its task kept the original path.
      2. Windows root-relative and drive-relative paths escaped src.
      3. An empty or conflicting task fragment packaged and failed only at deploy.
    • Round 2 found three feat: add AgenticComponentActivity for agent-authored bundle components #96 issues, all fixed in 1b5a763:

      1. An authored file could still collide with a generated or setup notebook.
      2. A component inside IfCondition, Switch or ForEach lost its pipeline resources.
      3. dab_writer imported a private helper, now public as content_signature.
    • Deferred: Round 2's fourth finding, namespacing agentic resources in shared multi-DAG Airflow bundles.

    • Round 3 (on 839b2c6) found five. Three were feat: add AgenticComponentActivity for agent-authored bundle components #96 issues, all fixed in 3586b9d:

      1. A Python, wheel or JAR task with no compute packaged and failed only at deploy. It is now refused.
      2. A file entry with neither content nor binary_content packaged as an empty file. It is now refused.
      3. Authored text gained a trailing newline, and would have gained Windows line endings. It is now written byte for byte.

      The no-mistakes review then tightened the compute rule, in 1151a0e and 07eae42:

      • a job_cluster_key must name a cluster flowx defines;
      • spark_submit_task and dbt_task need compute too;
      • a spark_submit_task runs only on a new cluster.

      Round 3's other two findings join the deferred Airflow namespacing item: environment and pipeline paths that aren't rewritten, and paths that can be rewritten twice.

  • Round 4, a review in the style of Greg's refactor: add source-neutral discovery graph contract and shared lineage core #84 and feat(adf): save source graphs with dataset lineage at ADF discover #94 reviews (fail closed, no unknown keys riding along, no false links, docs that match the code), plus an independent Codex pass with the same checklist, found seven feat: add AgenticComponentActivity for agent-authored bundle components #96 issues, all fixed test first in 24aa295 and 6ec268d:

    1. A null, empty or second compute key counted as naming compute.
    2. A for_each_task with no body, or a body without a task_key, packaged.
    3. Unknown fields on a file entry were dropped (an encoding flag wrote base64 text).
    4. Authored paths that differ only in case, or a file that is also a directory, clashed on disk.
    5. A generated file's added newline could hide an authored file at the same path.
    6. A merged component dropped the placeholder's notifications, compute mode and lineage.
    7. A for_each_task body inside a component was cleaned up as if generated.

    The no-mistakes review of that round then refused flowx-owned fields in a loop body, backslash paths, mistyped notebook and parameter fields, unknown resource and component keys, and libraries or existing_cluster_id on the component (6701a13, f2c8df7, 7e0222d, ca3d7c7). Outside feat: add AgenticComponentActivity for agent-authored bundle components #96 and left as follow-ups: an unknown IR type reloads as a placeholder, and main's generic merge drops source metadata for other replacement types.

  • The no-mistakes review then tightened the contract:

    • D4 and "written as given": 38531a4, 9234665, b99d98b and 1236e5c.
    • Payload validation and identical-duplicate pipeline resources: 38531a4 and bb488ec.
    • Component-carried job environments for serverless tasks: bf32de8, the captain's choice.
    • Docs matched to the code: a9ab36f, ddbba41 and 540efe3.

Merge order

Merge #84 first, then #94 retargeted to main, then #95, then this PR (retargeted to main once #95 has merged), then #97.

This pull request and its description were written by Isaac.

matthewmoorcroft and others added 8 commits September 16, 2026 17:25
Add AgenticComponentActivity to the IR: a generic passthrough for source
activities the typed engine cannot express, carrying authored bundle files,
resources and a raw Databricks task fragment. Wire it through the workflow
preparer dispatch, IR (de)serialization and the DAB writer (keeping agentic
files within the bundle source tree and honouring write_to_bundle_root), and
add a bundle-invariants check that every pipeline task reference resolves.

Consolidates PR #91 (generic agentic bundle component passthrough).

Co-authored-by: Isaac <no-reply@databricks.com>
Propagate the flowx-enrich skill + discover-chains-into-enrich reframe up
the stack. No new skill content on this branch.

Co-authored-by: Isaac <no-reply@databricks.com>
…ponent

Propagate discover warning/units + enrich edge_type guidance + GA-grounding up
the stack. No edits originate on this branch.

Co-authored-by: Isaac <no-reply@databricks.com>
…-component

Propagate the --adf-source-path warning correction and the enrich GA-grounding
rework up the stack. No edits originate on this branch.

Co-authored-by: Isaac <no-reply@databricks.com>
…to stack/agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
…stack/agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
…ck/agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
…ease_state examples) into stack/agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 2 commits October 5, 2026 11:56
…ssthrough

An AgenticComponentActivity's authored task fragment could override
depends_on, run_if, timeouts and retries; only task_key was forced. Reject a
fragment that sets any flowx-owned field and apply the activity's own fields
last, so the agent supplies only what the task runs.

Co-authored-by: Isaac <no-reply@databricks.com>
…h motifs, shared emitter) into stack/agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft matthewmoorcroft changed the title Phase 1 stack #4: agentic-component escape hatch Phase 1 stack #4: agentic-component escape hatch, with task identity, dependencies and run policy owned by flowx Oct 5, 2026
matthewmoorcroft and others added 5 commits October 5, 2026 21:09
…o stack/agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
…agentic-component

Carries #94 and #95 forward: dataset identity fixes, the shared lineage parser, one
shared canonical hash (discovery_serde.canonical_sha256), hardened insights validation,
and the enrich wording and docs.

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft matthewmoorcroft changed the title Phase 1 stack #4: agentic-component escape hatch, with task identity, dependencies and run policy owned by flowx feat: add AgenticComponentActivity for agent-authored bundle components Oct 6, 2026
matthewmoorcroft added a commit that referenced this pull request Oct 7, 2026
…/routing-agentic

Carries #94, #95 and #96 forward: dataset identity fixes, the shared lineage parser,
one shared canonical hash (discovery_serde.canonical_sha256), hardened insights
validation and enrich wording, and the agentic component resource-key checks.

Conflicts kept both wordings: the AGENTS.md discover line matches phase1-integration,
and the MCP tool description takes #95's corrected enrich text with this branch's
route and fill_agentic entries. README listed source_insights.json twice after the
auto-merge; the two lines are combined into one that keeps both descriptions.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 7, 2026
…integration

Brings in the final heads of #95 (5fa3ea2), #96 (b508ecc) and #97 (7cc139f), so the
integration branch carries every Phase 1 PR as validated: insights hardening and the
shared canonical hash, the agentic component checks, and the routing replay model.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 4 commits October 7, 2026 19:31
…s tasks

Two agentic components that author different content at the same file path now fail
before anything is written; the notebook writer used to rename the second file while its
task still pointed at the first, so it ran the other component's code. Identical shared
files still package. Authored paths with a Windows root or drive ("\tmp\x.py",
"C:x.py") are rejected like other escapes. An authored task fragment must carry exactly
one executable payload (a single "<kind>_task" key), so an empty or conflicting fragment
fails at package time instead of at deploy. Each fix has a regression test that fails on
the previous code.

Co-authored-by: Isaac <no-reply@databricks.com>
…agentic-component

Carries #84's, #94's and #95's Codex fixes forward, including the authored_against binding
for enrich and the paired source_insights.json and inventory.json write.

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft matthewmoorcroft changed the title feat: add AgenticComponentActivity for agent-authored bundle components feat: add agentic component activity for agent-authored bundle components Oct 7, 2026
…agentic-component

Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft matthewmoorcroft changed the title feat: add agentic component activity for agent-authored bundle components feat: add AgenticComponentActivity for agent-authored bundle components Oct 8, 2026
matthewmoorcroft and others added 6 commits October 8, 2026 22:32
…tic-component

Co-authored-by: Isaac <no-reply@databricks.com>
…fore any tests or lint ran. They timed out downloading packages from `pypi-proxy.cloud.databricks.com`, a Databricks-internal mirror that public GitHub runners can't reach. The cause is in this PR: commit 08eb721 rewrote all 708 package URLs in `uv.lock` to point at that mirror, while the base ddc78d5 has none. The workflow's cleanup step only rewrites index URLs ending in `/simple`, so the `/packages/...` download links stayed pointed at the mirror. Rule broken: the committed `uv.lock` must resolve through the public index (pypi.org / files.pythonhosted.org). `uv.lock` is the only lockfile the PR changes, and no other tracked file refers to an internal mirror. Fix: restored `uv.lock` to its ddc78d5 content (`git checkout ddc78d5 -- uv.lock`; staged, not committed). Only the URLs and file sizes had changed: every package name and version is the same, so the lockfile resolves exactly as before. Checks run locally with UV_FROZEN=1, so `uv run` couldn't re-lock against the local index: - `make test`: 1494 passed, 8 skipped. - `make fmt`: ruff made no changes, lint passed, mypy found no issues in 125 files. - `make requirements`: `requirements.txt` unchanged, so CI's freshness check will pass. - `uv.lock` unchanged after all three runs, with 0 mirror URLs
… write authored text as given

Codex round 3 on #96:
- A spark_python_task, python_wheel_task or spark_jar_task fragment must name its compute
  (environment_key, job_cluster_key, existing_cluster_id or new_cluster). flowx binds a cluster
  only for notebooks, so these packaged with no compute and failed at deploy.
- An authored file must set exactly one of content or binary_content; a bare path no longer
  packages as an empty file.
- Authored text files are written byte for byte: no added trailing newline and no platform line
  endings. Generated notebooks keep the trailing newline.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 9, 2026
… into stack/routing-agentic

Conflicts kept both parents' behaviour:
- ir_serde: #97's _locate_task split plus #96's placeholder-owned fields for an
  AgenticComponentActivity replacement.
- mcp/server.py, app/README.md: #96's enrich prepare/apply text plus #97's
  route and fill_agentic entries.
- README.md, architecture.mdx: #96's wording.

#95's rename reaches #97's own code: the plan, the route audit, routing and
their tests now bind to agentic_insights.json and its agentic_insights_sha256
self-hash key (routing read the old key, so after the merge every plan would
have recorded a null insights hash).

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 9, 2026
…gentic

Carries #96's tightened agentic-component checks (compute-less Python, JAR,
submit and dbt components; undefined job clusters; authored text written as
given). No conflicts.

Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft and others added 7 commits October 18, 2026 21:58
…ields and on-disk path clashes

Greg-checklist review of the agentic component (with an independent Codex pass):

- A compute key set to null or empty, or more than one compute key, is refused; a list
  environment_key no longer crashes the writer.
- A for_each_task must carry its body task with a task_key, and the body is marked
  authored so its parameters are written as given.
- An authored file entry with a field other than path, content and binary_content is refused.
- Authored paths that differ only in letter case, or a file path another file uses as its
  directory, are refused before anything is written.
- The content signature compares the text that lands on disk, so a generated file that would
  gain a trailing newline no longer matches an authored file without one.

Co-authored-by: Isaac <no-reply@databricks.com>
A component merged over a placeholder lost the placeholder's notifications, compute mode,
description, lineage and source definition, so the documented "source activity's collapsed
notifications" were never added on the merge path. The component now keeps each of those
fields the agent leaves out; a value the agent sets wins.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
matthewmoorcroft added a commit that referenced this pull request Oct 19, 2026
Brings in #96's component contract: unknown keys on components, resources
and loop bodies are refused, and so are libraries and existing_cluster_id
on a component. Conflicts kept #97's _locate_task and replace_task_by_name
split and added #96's inherited placeholder fields to it. The route skill's
AgenticComponentActivity example now follows the contract, and a new test
shows fill-agentic refuses a component that puts libraries or a cluster on
the activity, with nothing stored.

Co-authored-by: Isaac <no-reply@databricks.com>

@ghanse ghanse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task-ownership rules, collision checks and byte-for-byte writes look solid. Fix the parameter rewrite on nested agent-written loops before merging.

pipeline_resources.extend(child_prepared.pipeline_resources)
environments.extend(child_prepared.environments)

normalize_inner_task_params(inner_tasks)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: normalize_inner_task_params still rewrites the body of an agent-written for_each_task, so {{input.x}} becomes {{job.parameters.x}}, which points at the outer loop's parameters. Skip bodies marked _authored here and at line 191.

# authored:  {"table": "{{input.table_name}}"}
# after:     {"table": "{{job.parameters.table_name}}"}  # wrong loop

Comment on lines +1165 to +1179
def _collect_environments(workflow: PreparedWorkflow) -> list[dict[str, Any]]:
"""Returns every job environment carried by *workflow* and its inner jobs.

Each entry is an ``{environment_key, spec}`` dict. An environment declared
more than once with the same key and spec appears once, so it is written
once per job that uses it.
"""
environments: list[dict[str, Any]] = []
for current in [workflow, *workflow.inner_workflows]:
for environment in current.environments:
if environment not in environments:
environments.append(environment)
return environments


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environments are written as-is, but the multi-DAG rename only updates paths inside tasks. So ../src/libraries/foo.whl in an environment points at the old location after combine. The PR description already defers this, so a TODO or follow-up issue is enough.

matthewmoorcroft and others added 4 commits October 20, 2026 06:28
…a ForEach job

Inside a ForEach that runs as its own job, the inner-job pass rewrote the body of an authored
for_each_task too, so {{input.x}} became {{job.parameters.x}} (the outer loop's parameters), and
the scan forwarded x from the outer loop as an inner job parameter. Both now skip bodies marked
_authored. Adds a TODO where environments are collected: the combined multi-DAG rename does not
rewrite ../src paths inside an environment spec.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
@matthewmoorcroft

Copy link
Copy Markdown
Member Author

Thanks Greg, both addressed in 3406913:

  • Nested agent-written loops (blocking, r4321169995): fixed in 07a36c7, ccfb051 and 415309d. Inside a ForEach that runs as its own job, normalize_inner_task_params now skips a for_each_task body marked _authored, so your example {"table": "{{input.table_name}}"} stays as written, and {{input.x}} is no longer forwarded from the outer loop. That covers both call sites in for_each.py. A {{job.parameters.X}} the authored loop's inputs or body names is still forwarded from the enclosing job. An authored loop that is a ForEach's only child now runs in the inner job too, instead of nesting one For each inside another. Regression test: test_authored_loop_body_inside_a_for_each_job_keeps_its_own_input_references, covering all three ForEach shapes with a notebook and a serverless Python body; it fails before the fix.
  • Environment paths after the multi-DAG rename (r4321170002): added a TODO in 07a36c7 where environments are collected (_collect_environments). It stays with the deferred Airflow multi-DAG namespacing follow-up, as the description says.

matthewmoorcroft added a commit that referenced this pull request Oct 20, 2026
Brings in #96's round three with #95 6ed0b4b, #94 337d2d6 and #84 5ebdfcb
merged in. No conflicts.

Co-authored-by: Isaac <no-reply@databricks.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants