feat(sdk): create_ai_agent writes behaviors, and agent inputs prepare them - #704
Merged
Merged
Conversation
… 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>
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
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
requested changes
Sep 23, 2026
adriannoes
left a comment
Collaborator
There was a problem hiding this comment.
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_agenttreats an update that comes back with no behaviors as configured success. The mutation already selectsbehaviorsandinstruction. The service keeps only the uuid. See the thread onpackages/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 preservesdisabled_at, so the shell stays disabled. See the thread onpackages/sdk/src/pipefy_sdk/exceptions.py:27.
Decisions I would make
- JSON
uuid: nullstill becomes the stringNone, 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
devwill not auto-close the linked issue, because the default branch ismain. Close it by hand when this lands ondev, or when the change reachesmain. - 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=Falseis a flag the update adapter actually reads. - MCP partial failure still names
toggle_ai_agent_status. Placeholder expansion sits on the input models, and aBehaviorInputinstance 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.
…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>
Collaborator
Author
|
On the three decisions in the review body:
|
adriannoes
approved these changes
Sep 23, 2026
adriannoes
left a comment
Collaborator
There was a problem hiding this comment.
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.
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PipefyClient.create_ai_agentdropped the agent's instruction and behaviors.CreateAiAgentInputalready required both fields, butAiAgentService.create_agentsent onlyname,repoUuid, anddisabledAt. An SDK caller that followed thepipefy-ai-agentsskill got an empty, disabled agent and no error. Only the MCP tool and the CLI chained the update that writes the behaviors.create_ai_agentnow creates the agent and then callsupdate_ai_agentwithpreserve_disabled_at=False, as the MCP tool did. If the update fails, it raises the newAiAgentConfigureError(PipefyError). The error carriesagent_uuidanddisabled_at, and the update's error is its__cause__.CreateAiAgentInputandUpdateAiAgentInputnow prepare raw behavior dicts while they validate:template_params/placeholdersandinstruction_templateare 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) andpackages/sdk/src/pipefy_sdk/models/ai_agent.py(the two BeforeValidators) first.Where the issue was wrong. The issue proposed to add
instructionandbehaviorsas optional fields, and to run the prep inside the client methods. Neither works as written:behaviorshasmin_length=1). The bug was the dropped payload, so the fields stay required.BehaviorInputignores extra keys, sotemplate_paramsis gone before the client runs. The prep runs as BeforeValidators on the two agent input models instead. ABehaviorInputinstance 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:
createAiAgenttakes the sameAiAgentInputas the update, includingbehaviorsandinstruction. One call is not enough: the create service setsdisabled_atto now when the input has nodisabled_atkey, and only the update service clears it, when a behavior is active. So the client keeps the two calls, and the update omitsdisabledAtunless the caller setdisabled_at. The create service does write behaviors when it receives them, and it checks for the presence of thedisabled_atkey, so a single create with an explicitdisabledAt: nullcan work. Nobody has verified how that null arrives through GraphQL, so this PR keeps the two calls that the MCP tool already used.manage_ai_agentspermission on the pipe. That requirement is unchanged.repo_uuidis the pipe UUID, and the agent is addressed by its UUID.Behavior boundaries.
{{placeholder}}with no value now returns PydanticValidationErrortext instead of the plainValueErrortext. Six tool paths already return rawValidationErrortext, and MCP: tools return raw Pydantic ValidationError text from SDK input models #703 tracks a shared formatter for all of them.agent createoutput 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'smessage (CODE)form. If the update fails with a plainValueError(the API response has noagent.uuid),agent createnow exits 1 instead of 2.pipefy_mcp.tools.behavior_placeholder_interpolationis removed. It was a re-export with no importer left in this repo or in the sibling repos. Its 28 tests move topackages/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 noPIPEFY_*variable setuv 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.main, so it cannot run this branch. Rows 4 and 7 below run the branch's MCPcreate_ai_agenttool in-process against a real authenticated client instead. The CLI rows use the branch's CLI, whoseagent createnow calls onlyPipefyClient.create_ai_agent.Live check on a test pipe:
agent create: behavior withtemplate_params+instruction_template, token aliases in both instructions{{…}}filled, aliases canonical,referencedFieldIdsfilledactive: true; agent instruction%{field:…190}; behavior instruction%{field:…190}/%{field:…212};pipeId{{pipe}}filled;referencedFieldIdshas both IDsagent create --inactive, same payloaddisabled_atset,active: falsedisabled_atset,active: falseagent updateon the inactive agent, instruction%{<digits>}%{field:<digits>},disabledAtunchanged,active: falsecreate_ai_agent(branch, in-process), same payloadsuccess: true,active: true, tokens canonical,referencedFieldIdsfilledagent create:{{missing}}with no valuetemplate_paramsagent create --no-strictwith an unknownactionType, so the update failscreate_ai_agent, same failing payloadagent_uuid,disabled_at, enriched error, toggle hintsuccess: false,agent_uuidset,disabled_atset,active: false, "Behaviors sent (1)" enrichment and toggle hintdisabledAtset,behaviors: nullsuccess: true; a read fails afterwardssuccess: truefor both; the read returns an errorDocs / skills
docs/parity.mdupdated when MCP ↔ CLI coverage changedskills/updated in this PR (or a paired PR)docs/parity.mddoes not change, because no MCP tool or CLI command is added or renamed.docs/sdk/README.mdgains an "AI agents" section and anAiAgentConfigureErrorentry under Errors.CHANGELOG.mdgains an Unreleased "Changed" entry.skills/ai-agents/pipefy-ai-agents/SKILL.mdchanges 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
git commit -s)COMPLIANCE.mdwhen applicableCloses #695