Skip to content

feat(sdk): create_ai_agent writes behaviors, and agent inputs prepare them - #704

Merged
mocha06 merged 5 commits into
devfrom
rc-dev/feat/695-sdk-create-agent-behaviors
Sep 23, 2026
Merged

mocha06 merged 5 commits into
devfrom
rc-dev/feat/695-sdk-create-agent-behaviors

Conversation

@mocha06

@mocha06 mocha06 commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • PipefyClient.create_ai_agent dropped the agent's instruction and behaviors. CreateAiAgentInput already required both fields, but AiAgentService.create_agent sent only name, repoUuid, and disabledAt. An SDK caller that followed the pipefy-ai-agents skill got an empty, disabled agent and no error. Only the MCP tool and the CLI chained the update that writes the behaviors.
  • create_ai_agent now creates the agent and then calls update_ai_agent with preserve_disabled_at=False, as the MCP tool did. If the update fails, it raises the new AiAgentConfigureError(PipefyError). The error carries agent_uuid and disabled_at, and the update's error is its __cause__.
  • CreateAiAgentInput and UpdateAiAgentInput now prepare raw behavior dicts while they validate: template_params / placeholders and instruction_template are expanded, and instruction token aliases are normalized. The MCP tools and CLI commands no longer do their own prep or their own create + update chain.

Read packages/sdk/src/pipefy_sdk/client.py (create_ai_agent) and packages/sdk/src/pipefy_sdk/models/ai_agent.py (the two BeforeValidators) first.

Where the issue was wrong. The issue proposed to add instruction and behaviors as optional fields, and to run the prep inside the client methods. Neither works as written:

  • The fields already existed and were required (behaviors has min_length=1). The bug was the dropped payload, so the fields stay required.
  • The client receives a model that is already validated, and BehaviorInput ignores extra keys, so template_params is gone before the client runs. The prep runs as BeforeValidators on the two agent input models instead. A BehaviorInput instance passes through unchanged, because expansion is not idempotent: a substituted value that contains {{x}} fails a second pass.

Contract. No new GraphQL operation. Each argument was traced to the layer that enforces it:

  • createAiAgent takes the same AiAgentInput as the update, including behaviors and instruction. One call is not enough: the create service sets disabled_at to now when the input has no disabled_at key, and only the update service clears it, when a behavior is active. So the client keeps the two calls, and the update omits disabledAt unless the caller set disabled_at. The create service does write behaviors when it receives them, and it checks for the presence of the disabled_at key, so a single create with an explicit disabledAt: null can work. Nobody has verified how that null arrives through GraphQL, so this PR keeps the two calls that the MCP tool already used.
  • Both mutations need the manage_ai_agents permission on the pipe. That requirement is unchanged.
  • Identifier forms are unchanged: repo_uuid is the pipe UUID, and the agent is addressed by its UUID.

Behavior boundaries.

  • MCP tool output is unchanged, except in one case. A {{placeholder}} with no value now returns Pydantic ValidationError text instead of the plain ValueError text. Six tool paths already return raw ValidationError text, and MCP: tools return raw Pydantic ValidationError text from SDK input models #703 tracks a shared formatter for all of them.
  • CLI agent create output keys are unchanged. When the update fails, the CLI now prints the created agent's UUID, which it lost before. GraphQL errors keep the CLI's message (CODE) form. If the update fails with a plain ValueError (the API response has no agent.uuid), agent create now exits 1 instead of 2.
  • pipefy_mcp.tools.behavior_placeholder_interpolation is removed. It was a re-export with no importer left in this repo or in the sibling repos. Its 28 tests move to packages/sdk/tests/test_behavior_placeholders.py, because they were the only tests of the SDK helpers.

Test plan

  • uv run pytest -m "not integration": 4798 passed, 5 skipped, with no PIPEFY_* variable set
  • uv run ruff check . && uv run ruff format --check .
  • cd packages/mcp && uv run lint-imports: 2 contracts kept. scripts/bump_version.py verify, the skill-ref linter, and the Cursor-plugin linter pass.
  • Manual smoke via Cursor MCP: not run. The session's MCP server runs main, so it cannot run this branch. Rows 4 and 7 below run the branch's MCP create_ai_agent tool in-process against a real authenticated client instead. The CLI rows use the branch's CLI, whose agent create now calls only PipefyClient.create_ai_agent.

Live check on a test pipe:

# Scenario Expected Observed Verdict
1 CLI agent create: behavior with template_params + instruction_template, token aliases in both instructions Active agent; {{…}} filled, aliases canonical, referencedFieldIds filled active: true; agent instruction %{field:…190}; behavior instruction %{field:…190} / %{field:…212}; pipeId {{pipe}} filled; referencedFieldIds has both IDs Pass
2 CLI agent create --inactive, same payload disabled_at set, active: false disabled_at set, active: false Pass
3 CLI agent update on the inactive agent, instruction %{<digits>} Instruction canonical, disabled state kept Instruction %{field:<digits>}, disabledAt unchanged, active: false Pass
4 MCP create_ai_agent (branch, in-process), same payload Success, prep applied success: true, active: true, tokens canonical, referencedFieldIds filled Pass
5 CLI agent create: {{missing}} with no value Exit 2 before any API call; error names template_params Exit 2, "Missing template parameter 'missing' … (set template_params / placeholders …)" Pass
6 CLI agent create --no-strict with an unknown actionType, so the update fails Exit 1; stderr names the created agent and the API reason Exit 1, "AI Agent was created, but writing its instruction and behaviors failed: … actionType (Expected … to be one of: …)" Pass
7 MCP create_ai_agent, same failing payload Partial-failure payload with agent_uuid, disabled_at, enriched error, toggle hint success: false, agent_uuid set, disabled_at set, active: false, "Behaviors sent (1)" enrichment and toggle hint Pass
8 Read the agent from row 6 Exists, disabled, no behaviors disabledAt set, behaviors: null Pass
9 Delete the throwaways from rows 6 and 7 success: true; a read fails afterwards success: true for both; the read returns an error Pass

Docs / skills

  • docs/parity.md updated when MCP ↔ CLI coverage changed
  • Affected skills/ updated in this PR (or a paired PR)
  • No docs/skills update needed

docs/parity.md does not change, because no MCP tool or CLI command is added or renamed. docs/sdk/README.md gains an "AI agents" section and an AiAgentConfigureError entry under Errors. CHANGELOG.md gains an Unreleased "Changed" entry. skills/ai-agents/pipefy-ai-agents/SKILL.md changes two lines: the partial-failure step names the SDK error, and the template-params section says that the SDK input models interpolate too.

Legal / contributions

  • Commits include DCO sign-off (git commit -s)
  • Regulated-domain skills include COMPLIANCE.md when applicable

Closes #695

… them

PipefyClient.create_ai_agent sent only name, repoUuid and disabledAt, so
it dropped the required instruction and behaviors of CreateAiAgentInput
and returned an empty, disabled agent. Only the MCP tool and the CLI
chained the update that writes them.

The client now creates the agent and chains update_ai_agent with
preserve_disabled_at=False. The API stamps disabledAt on a new agent and
only an update with an active behavior clears it, so the chain is needed.
When the update fails, the new AiAgentConfigureError carries the created
agent_uuid and chains the update's error.

CreateAiAgentInput and UpdateAiAgentInput expand template_params /
instruction_template and normalize instruction token aliases on raw
behavior dicts while they validate. The client cannot do this, because
BehaviorInput validation already drops the template keys. BehaviorInput
instances pass through, since expansion is not idempotent.

The MCP tools and CLI commands drop their own prep and chain. The unused
pipefy_mcp.tools.behavior_placeholder_interpolation re-export is removed,
and its tests move to the SDK next to the helpers they cover.

Closes #695

Signed-off-by: mocha06 <52426811+mocha06@users.noreply.github.com>
@mocha06 mocha06 added the enhancement New capability or intentional behavior change (not a bugfix) label Sep 23, 2026
Build UpdateAiAgentInput inside the try, so every failure after the create raises AiAgentConfigureError with the created agent_uuid. The CHANGELOG entry names the literal {{name}} rejection for SDK callers and the double-expansion case for callers that still expand before validation.

Signed-off-by: mocha06 <52426811+mocha06@users.noreply.github.com>
@mocha06
mocha06 requested a review from adriannoes September 23, 2026 16:59
@mocha06
mocha06 requested review from adriannoes and removed request for adriannoes September 23, 2026 17:17
Pydantic coerces any iterable into list[BehaviorInput], but the placeholder step ran only for a list, so a tuple or generator silently dropped template_params and instruction_template. Expand every iterable except str, bytes and dict.

Signed-off-by: mocha06 <52426811+mocha06@users.noreply.github.com>

@adriannoes adriannoes 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.

Reviewed at c8ad5b44: package tests, the changed agent modules, and two refutations against a fake GraphQL executor. No live create.

Verdict: needs changes into dev.

Required before merge

  • create_ai_agent treats an update that comes back with no behaviors as configured success. The mutation already selects behaviors and instruction. The service keeps only the uuid. See the thread on packages/sdk/src/pipefy_sdk/client.py:1432.
  • When configure fails, the CLI and SDK error name the uuid and the GraphQL code, then point recovery at update_ai_agent. That update preserves disabled_at, so the shell stays disabled. See the thread on packages/sdk/src/pipefy_sdk/exceptions.py:27.

Decisions I would make

  • JSON uuid: null still becomes the string None, and the new chain will try to configure that string. The guard predates this PR. I would leave it off the merge gate and reject a null uuid in a follow-up.
  • Merging to dev will not auto-close the linked issue, because the default branch is main. Close it by hand when this lands on dev, or when the change reaches main.
  • A live read after an active create did not run. Unit tests only show that the payload omits disabledAt. I would not treat that as proof the API cleared the stamp.

Optional

Your call on all of these.

  • 1 smaller pointer is on its line in the diff.

What worked well

  • The create-then-update chain lives on the client, and preserve_disabled_at=False is a flag the update adapter actually reads.
  • MCP partial failure still names toggle_ai_agent_status. Placeholder expansion sits on the input models, and a BehaviorInput instance is not expanded twice.
Review path

Tier standard, trust trusted, reviewed at 4def1e25...c8ad5b44.

Gate Result
CI green
Local pytest sdk/mcp/cli 3799 passed
Targeted agent modules 331 passed
The two blocking claims both reproduced, three runtime attempts each
Codex double-check no additional findings
Live create blocked, no mutation consent
Flag, architecture, callers, API shape, vocab, disclosure pass
Security hunt not run. Diff has no auth or install surface

Not covered: a live active create, then a read of disabledAt. Integration tests were not run locally.

Comment thread packages/sdk/src/pipefy_sdk/client.py
Comment thread packages/sdk/src/pipefy_sdk/exceptions.py
Comment thread packages/cli/tests/test_cli_agent_lifecycle.py Outdated
…gureError

Answers the review thread on exceptions.py:27. The create leaves the agent disabled and a routine update_ai_agent preserves that, so the error, its docstring and the README Errors bullet now say so and name toggle_ai_agent_status.

Signed-off-by: mocha06 <52426811+mocha06@users.noreply.github.com>
Answers the review thread on test_cli_agent_lifecycle.py:48. The tests assert only the CreateAiAgentInput the command builds; the SDK facade tests own the create-then-update chain assertions.

Signed-off-by: mocha06 <52426811+mocha06@users.noreply.github.com>
@mocha06

mocha06 commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

On the three decisions in the review body:

@adriannoes adriannoes 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 follow-up commits address the recovery envelope and the CLI lifecycle tests; the API upsert argument on the configured-success thread is enough for me. Thanks @mocha06 for the clear replies and the quick fixes.

Verdict: merge-ready into dev.

@mocha06
mocha06 merged commit 7aa32b6 into dev Sep 23, 2026
6 checks passed
@adriannoes
adriannoes deleted the rc-dev/feat/695-sdk-create-agent-behaviors branch September 23, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New capability or intentional behavior change (not a bugfix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants