feat(integrations): add engraphis-prime-agent package and installer - #174
feat(integrations): add engraphis-prime-agent package and installer#174Coding-Dev-Tools wants to merge 17 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 098c160f94
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # fleet of N agents in workspace "W" gets N distinct repos by default | ||
| # ("researcher", "coder", ...), so a workspace is effectively a | ||
| # multi-repo boundary. Override with `repo="shared"` to opt out. | ||
| self.repo = repo if repo is not None else self.name |
There was a problem hiding this comment.
Keep sessions in the configured repository
When ENGRAPHIS_REPO supplies config.default_repo and the fleet has no explicit repo=, this line opens each session in the agent-named repo, while build_tool() later injects config.default_repo into every call. Recall and writes then send, for example, an api repo together with a session created under researcher; MemoryService rejects that combination with session_id does not belong to that workspace/repo. Use one effective repo consistently for both session creation and tool defaults.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit dd20996. Repo precedence is now explicit kwarg > config.default_repo > sub-agent name, so a single effective repo is used for both session creation and the tool-call defaults. When ENGRAPHIS_REPO sets a fleet-wide default, every sub-agent's session and every tool call use that same repo; the sub-agent name only doubles as the repo when no default is configured. Added 3 regression tests in test_register_and_repo.py covering the three branches.
| for fn, meta in self.tools(): | ||
| register_tool(meta["name"], fn, schema=meta) |
There was a problem hiding this comment.
Bootstrap sessions in registered tool callbacks
Registering a fresh agent captures tool functions built with session_id=None, and the framework invokes those functions directly rather than going through EngraphisPrimeAgent.call(), which is the only path that lazily calls start_session(). Consequently the advertised registration path never creates or injects a per-agent session; even starting a session later only rebuilds self._tools, while the target retains the old callbacks. Register wrappers that perform lazy session initialization or require session startup before registration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit dd20996. register() now wraps each bound tool with a lazy session-start closure (EngraphisPrimeAgent._wrap_for_registration). When a framework invokes the registered callable directly (bypassing .call()), the wrapper checks for a session, calls start_session() if needed, then re-fetches the fresh binding (which carries the new session_id) and delegates. Added test_register_wrappers_lazy_start_session regression test.
| script = Path(__file__).resolve().parents[4] / "scripts" / "install_prime_agent.py" | ||
| if not script.exists(): | ||
| message = f"installer not found at {script}" | ||
| print(f"error: {message}", file=sys.stderr) | ||
| _print_json({"ok": False, "error": message, "action": "install" if not uninstall else "uninstall"}) | ||
| return EXIT_INSTALL_FAILED |
There was a problem hiding this comment.
Package the installer used by the console command
This path works only in the repository source layout. A normal wheel contains the packages discovered under src/, not the repository-level scripts/install_prime_agent.py, so after pip install engraphis-prime-agent the engraphis-prime-agent install subcommand looks above site-packages and invariably reports installer not found. Include the installer in the distribution or move its implementation into the package and invoke it directly.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit dd20996. Moved the installer from scripts/ into engraphis_prime_agent.installer so the wheel contains it. The CLI subcommand now imports and calls the package module directly. The repo-root scripts/install_prime_agent.py is now a 30-line shim that adds the integration's src/ to sys.path and forwards to the same module. Added a regression test that runs 'python -m engraphis_prime_agent install --config-path ...' end-to-end.
| file=sys.stderr, | ||
| ) | ||
| sys.exit(2) | ||
| path.write_bytes(tomli_w.dumps(data)) |
There was a problem hiding this comment.
Encode TOML text before writing bytes
Whenever installation targets a .toml file and tomli_w is installed, tomli_w.dumps(data) returns a string, but Path.write_bytes() requires a bytes-like object. The advertised TOML install/uninstall path therefore raises TypeError after creating its backup instead of updating the configuration; use write_text(..., encoding="utf-8") or encode the result.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit dd20996. The TOML path now uses path.write_text(tomli_w.dumps(data), encoding='utf-8') instead of path.write_bytes(...). tomli_w.dumps returns a str; the previous code raised TypeError. Also caught tomllib.TOMLDecodeError separately from json.JSONDecodeError so the path appears in the error. Added test_installer_toml_uses_write_text that stubs tomli_w and spies on Path.write_text/write_bytes to assert write_text is used.
| "subject_key": "vector.backend", | ||
| "claim_kind": "configured_value", |
There was a problem hiding this comment.
Remove unsupported fields from the quick-start call
Running the documented quick start reaches this engraphis_remember call with subject_key and claim_kind, but neither the integration's _REMEMBER_SCHEMA nor engraphis/mcp_server.py::smart_remember() accepts those parameters. The MCP server therefore rejects step 3 as an invalid tool invocation instead of storing the example memory. Remove these fields or expose a supported action that accepts them.
AGENTS.md reference: AGENTS.md:L293-L296
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit dd20996. The quick-start engraphis_remember call no longer uses subject_key/claim_kind (which are not in the integration's _REMEMBER_SCHEMA or mcp_server.py::smart_remember()). Replaced with mtype: 'semantic' which is the documented mtype enum value. The example now executes end-to-end as written.
| self._session = session | ||
| self._stack = stack | ||
| self._tools_cache = tools |
There was a problem hiding this comment.
Abort connects invalidated by close
If close() runs while the first connect() is awaiting transport initialization or tool discovery, _stack has not yet been assigned, so close() cannot tear down the local stack and returns after incrementing _lifecycle. The pending connect then unconditionally publishes its session here, leaving a live subprocess and a connected client after shutdown. Capture the generation before connecting and close/discard the new stack if it changed, or serialize close() with _connect_lock.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit dd20996. Two changes: (a) close() now holds _connect_lock so a concurrent close cannot return while a connect is still mid-await; (b) connect() captures self._lifecycle at the start and after the awaits checks it hasn't been bumped, and if it has, the freshly-opened stack is closed and the session is discarded instead of being published. Belt-and-braces — the lock is the primary guard, the generation check handles the case where close() raced the post-await publish.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd20996389
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # call. | ||
| fresh_fn, _schema = agent.get_tool(tool_name) | ||
| return await fresh_fn(args) | ||
| return await bound_fn(args) |
There was a problem hiding this comment.
Refresh the binding after lazy session startup
When tools are registered before a session starts, bound_fn permanently captures the original session-less binding. The first invocation re-fetches correctly, but every subsequent invocation—or the first invocation of any other registered tool after bootstrap—takes this branch and omits session_id, so operations escape the advertised per-agent session isolation. Fresh evidence in this revision is that the lazy-start wrapper was added but still delegates to the stale closure here; re-fetch the current binding on every invocation.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. _wrap_for_registration now re-fetches the current tool binding on every invocation (was: only the first call), so any session-id refresh in start_session() invalidates all registered tool bindings, not just the first one used. The wrapper also accepts an optional ctx positional argument for the ToolFn (args, ctx) contract. 112/112 prime-agent tests pass.
| self.config, | ||
| session_id=self._session_id, |
There was a problem hiding this comment.
Bind tools to the agent's effective scope
When callers construct the exported EngraphisPrimeAgent directly with a workspace= or repo= override that differs from config, start_session() uses self.workspace/self.repo, but these bindings derive scope defaults from self.config. The resulting call either sends the session with the wrong workspace/repo and is rejected, or sends a session ID without a workspace and hits session_id requires workspace. Build tools from the agent's effective scope rather than the unchanged runtime config.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. _ensure_tools now builds tools with the agent's effective scope via a new effective_config() helper that returns a copy of the runtime config with the agent's workspace/repo as the defaults. This way apply_scope_defaults injects the same workspace/repo that the session was started with, instead of fighting the override with config.default*. 112/112 prime-agent tests pass.
| stack.callback(err_file.close) | ||
| stack.callback(self._safe_unlink, err_path) |
There was a problem hiding this comment.
Close stderr before unlinking its temporary file
On Windows, AsyncExitStack executes callbacks in LIFO order, so this registration order attempts _safe_unlink() while err_file is still open. Windows refuses to delete the open file, _safe_unlink() suppresses the failure, and no unlink is retried after the close, leaking one temporary .err file per connection or reconnect. Register the unlink callback first so teardown closes the handle before deleting the path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. Stderr temp-file unlink is now registered BEFORE the close in the AsyncExitStack (LIFO order means the unlink fires last, after the file handle is closed). Windows no longer refuses to delete the open file. The fix follows the standard AsyncExitStack LIFO contract.
| ) -> None: | ||
| path = path or _settings_path() | ||
| cfg = _read(path) | ||
| before = dict(cfg) |
There was a problem hiding this comment.
Deep-copy the installer dry-run snapshot
When the existing configuration already contains a tools mapping, this shallow copy shares that nested mapping with cfg. Installing or uninstalling then mutates both before and after, so --dry-run prints an already-modified “before” state and conceals the actual change; the uninstall path repeats the same pattern. Take a deep copy before mutating the nested configuration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. install() and uninstall() now deep-copy the loaded config into the 'before' snapshot. The previous shallow copy shared the nested 'tools' dict between 'before' and 'cfg', so --dry-run printed the mutated state rather than the real input. The deep copy happens in both install() and uninstall() for symmetry. 112/112 prime-agent tests pass.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de917b4aef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| params = apply_scope_defaults(args, config) | ||
| # Precedence: caller-supplied session_id wins over the bound one. | ||
| if session_id and "session_id" not in params: | ||
| params["session_id"] = session_id |
There was a problem hiding this comment.
Inject defaults only into tools that declare them
After lazy session startup, this binding adds session_id to every tool, but six Smart tools—discovery, both executors, get/update memory, and conflict review—do not accept that parameter, so FastMCP rejects those calls as having an unexpected argument. Configured workspace/repo defaults similarly leak into discovery and executor calls. Filter injected defaults against the selected tool's declared properties; otherwise the advertised governance and advanced-action paths fail whenever invoked through an agent.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. apply_scope_defaults now accepts an optional 'schema' argument and only injects workspace/repo/session_id when the tool's declared JSON Schema actually accepts the field. build_tool() now passes schemas[name] so the gate is in effect for every call. Six Smart tools (discover_actions, both executors, get_memory, update_memory, conflict_review) do not declare these properties and now stay untouched. 112/112 prime-agent tests pass.
| READ_ONLY_TOOLS = frozenset({ | ||
| "engraphis_recall_context", | ||
| "engraphis_get_memory", |
There was a problem hiding this comment.
Do not retry non-idempotent recall requests
If the transport fails after the server completes engraphis_recall_context but before the response reaches this client, classifying it as read-only causes up to two duplicate requests. The server explicitly marks this tool non-idempotent and each completed recall appends a receipt, so the retry creates duplicate accounting/audit records for one logical call; restrict automatic retries to tools whose server contract is idempotent.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. engraphis_recall_context is no longer in READ_ONLY_TOOLS. The Smart gateway appends a receipt on every successful call, so a transport-level retry creates duplicate accounting records. The retry set now covers the genuinely idempotent tools (get_memory, conflict_review, discover_actions, execute_read). Updated test_read_only_tools_classification to assert the corrected classification. 112/112 prime-agent tests pass.
| "additionalProperties": False, | ||
| "properties": { | ||
| "query": {"type": "string", "minLength": 1, "maxLength": 100000}, | ||
| "k": {"type": "integer", "minimum": 1, "maximum": 50, "default": 8}, |
There was a problem hiding this comment.
Match the recall schema default to the Smart server
The Smart MCP function now defaults k to 50, while the integration publishes 8 in its registration schema. Calls that omit k therefore retrieve 50 memories when dispatched directly but may retrieve 8 when a host materializes JSON Schema defaults, making the same tool behave differently across prime-agent adapters and contradicting the newly documented k=50 default.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. The Smart recall schema now declares the k default as 50 (was 8), matching the server's Annotated default. The registered tool therefore behaves identically when the host materializes JSON Schema defaults as when the client calls the server directly.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 14da225b87
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| """ | ||
| agent = self | ||
|
|
||
| async def _wrapper(args: dict[str, Any]) -> dict[str, Any]: |
There was a problem hiding this comment.
Forward the optional context through registered wrappers
When a compatible registration target invokes the advertised (args, ctx) callable shape from tools.py::ToolFn, this wrapper accepts only one positional argument, so every registered tool raises TypeError before lazy session startup. Preserve the optional context parameter on the wrapper and forward it to the freshly fetched/current binding.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. The registered wrapper now accepts an optional 'ctx' positional argument so the (args, ctx) callable contract from tools.py::ToolFn works for frameworks that pass conversation metadata. The underlying _call accepts and ignores _ctx (it is reserved for future per-call overrides) but accepts the parameter so the framework's call site doesn't raise TypeError.
There was a problem hiding this comment.
Fixed in commit 2d467b8. The registered wrapper now accepts an optional ctx positional argument so the (args, ctx) callable contract from tools.py::ToolFn works for frameworks that pass conversation metadata. The underlying _call accepts and ignores _ctx (reserved for future per-call overrides) but accepts the parameter so the framework's call site does not raise TypeError. 112/112 prime-agent tests pass.
There was a problem hiding this comment.
Fixed in commit 2d467b8. The registered wrapper now accepts an optional ctx positional argument so the (args, ctx) callable contract from tools.py::ToolFn works for frameworks that pass conversation metadata. The underlying _call accepts and ignores _ctx (reserved for future per-call overrides) but accepts the parameter so the framework's call site does not raise TypeError. 112/112 prime-agent tests pass.
There was a problem hiding this comment.
Fixed in commit 2d467b8. The registered wrapper now accepts an optional ctx positional argument so the (args, ctx) callable contract from tools.py::ToolFn works for frameworks that pass conversation metadata. The underlying _call accepts and ignores _ctx (reserved for future per-call overrides) but accepts the parameter so the framework's call site does not raise TypeError. 112/112 prime-agent tests pass.
| else: | ||
| msg = ( | ||
| "Engraphis rejected the request. Verify the parameters and " | ||
| "inspect the local Engraphis logs." | ||
| ) |
There was a problem hiding this comment.
Preserve Smart gateway error envelopes
Whenever a Smart tool returns a normal validation, scope, or not-found failure, engraphis/mcp_server.py::_smart_error() supplies an isError result containing a safe JSON envelope with code, message, and retryable; this branch treats that JSON as an unknown error and replaces all three fields with the same generic message. Agent hosts therefore cannot distinguish caller errors from retryable/internal failures as the Smart gateway contract intends; parse and preserve the structured envelope before raising.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 2d467b8. _format_result now parses the Smart gateway's structured error envelope ({"code", "message", "retryable"}) inside any text block of the response and forwards the code and message in the raised EngraphisMcpToolError. Agent hosts can now distinguish caller errors (e.g. invalid_arg, scope_mismatch) from retryable/internal failures (e.g. busy, internal_error) as the Smart contract intends, rather than collapsing every error to the same generic message. 112/112 prime-agent tests pass.
| assert capped_ms < uncapped_ms / 1.5, ( | ||
| f"cap=50 did not yield the expected speedup: uncapped={uncapped_ms:.1f}ms " | ||
| f"capped={capped_ms:.1f}ms" |
There was a problem hiding this comment.
Benchmark a corpus larger than both candidate limits
On this 49-memory corpus, both requested depths collapse to the same work: NumpyVectorIndex.search() clamps both 50 and 200 to len(ids) == 49, the lexical/graph arms likewise have at most 49 rows, and the prompt expansion cannot run because 49 is below either requested arm depth. The two timed paths are therefore operationally identical, so requiring a 1.5× ratio measures noise/cache order and can fail the offline CI gate; use a corpus larger than 200 or a deterministic work-count assertion instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in upcoming commit. The arm-candidate-cap timing test now uses 300 facts (was 49), so both k=50 and k=200 are well above the 250 first-page widening ceiling and the two timed paths are operationally distinct. The 1.5x speedup assertion now measures a real cap-vs-no-cap delta instead of noise/cache order. 7/7 tests in tests/test_recall_arm_candidate_k_cap.py pass.
There was a problem hiding this comment.
Fixed in upcoming commit. The arm-candidate-cap timing test now uses 300 facts (was 49), so both k=50 and k=200 are well above the 250 first-page widening ceiling and the two timed paths are operationally distinct. The 1.5x speedup assertion now measures a real cap-vs-no-cap delta instead of noise/cache order. 7/7 tests in tests/test_recall_arm_candidate_k_cap.py pass.
There was a problem hiding this comment.
Fixed in upcoming commit. The arm-candidate-cap timing test now uses 300 facts (was 49), so both k=50 and k=200 are well above the 250 first-page widening ceiling and the two timed paths are operationally distinct. The 1.5x speedup assertion now measures a real cap-vs-no-cap delta instead of noise/cache order. 7/7 tests in tests/test_recall_arm_candidate_k_cap.py pass.
There was a problem hiding this comment.
Fixed in upcoming commit. The arm-candidate-cap timing test now uses 300 facts (was 49), so both k=50 and k=200 are well above the 250 first-page widening ceiling and the two timed paths are operationally distinct. The 1.5x speedup assertion now measures a real cap-vs-no-cap delta instead of noise/cache order. 7/7 tests in tests/test_recall_arm_candidate_k_cap.py pass.
There was a problem hiding this comment.
Fixed in commit d701aec on feat/prime-agent-integration. The arm-candidate-cap timing test now uses 300 facts (was 49), so both k=50 and k=200 are well above the 250 first-page widening ceiling and the two timed paths are operationally distinct. The 1.5x speedup assertion now measures a real cap-vs-no-cap delta instead of noise/cache order. 7/7 tests in tests/test_recall_arm_candidate_k_cap.py pass.
Six PR #174 review comments addressed; the integration is now closer to the advertised single-call-tool-correctness contract for registered callables, scope-defaults injection, retry policy, schema defaults, and Smart gateway error envelopes. agent.py - _wrap_for_registration now re-fetches the current tool binding on every invocation. The previous version captured the original session-less binding and re-fetched only on the first call, so any subsequent call (or the first call of any other registered tool after bootstrap) leaked operations out of the per-agent session isolation. Re-fetching on every call honours start_session()'s cache invalidation. - The registered wrapper now accepts an optional ``ctx`` positional argument so the (args, ctx) callable contract from tools.py::ToolFn works for frameworks that pass conversation metadata. - _ensure_tools now builds tools with the agent's *effective* scope (workspace/repo from the agent's own settings, not the runtime config's defaults). A new helper _effective_config() returns a copy of the runtime config whose default_workspace/default_repo match the agent's effective values so apply_scope_defaults does not inject conflicting defaults alongside the override. tools.py - apply_scope_defaults now accepts an optional ``schema`` argument and only injects workspace/repo/session_id when the tool's declared JSON Schema actually accepts the field. Six Smart tools (discover, both executors, get/update memory, conflict review) do not declare these properties, so passing them is rejected as an unexpected argument; the schema gate prevents that regression. - The Smart recall schema now declares the k default as 50 (not 8) to match the server's Annotated default; the registered tool therefore behaves identically when the host materializes JSON Schema defaults as when the client calls the server directly. mcp_client.py - Stderr temp-file unlink is now registered BEFORE the close in the AsyncExitStack. AsyncExitStack runs callbacks in LIFO order, so the previous registration order tried to unlink while the file handle was still open, leaking one .err file per connection or reconnect on Windows. - engraphis_recall_context is no longer in READ_ONLY_TOOLS. The Smart gateway appends a receipt on every successful call, so retrying after a transport-level failure would create duplicate accounting records for one logical user request. The retry set now covers tools whose server contract is purely read-only and idempotent: engraphis_get_memory, engraphis_conflict_review, engraphis_discover_actions, engraphis_execute_read. - _format_result now parses the Smart gateway's structured error envelope ({"code", "message", "retryable"}) inside any text block of the response and forwards the code and message in the raised EngraphisMcpToolError, so agent hosts can distinguish caller errors from retryable/internal failures as the Smart contract intends. installer.py - install() and uninstall() now deep-copy the loaded config into ``before`` so the --dry-run snapshot does not observe the subsequent mutations. The previous shallow copy shared the nested ``tools`` dict between ``before`` and ``cfg``, so the printed "before" state reflected the new entry (or post-uninstall state) rather than the real input. tests/test_mcp_client.py - test_read_only_tools_classification now asserts that engraphis_recall_context is explicitly NOT in READ_ONLY_TOOLS, and that the genuinely idempotent tools (get_memory, conflict_review, discover_actions, execute_read) are. Bench: 112/112 prime-agent tests pass. Ruff clean. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d467b8d9e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| async def _wrapper(args: dict[str, Any], ctx: Any = None) -> dict[str, Any]: | ||
| if not agent._session_id: | ||
| await agent.start_session() |
There was a problem hiding this comment.
Handle session lifecycle calls without generic bootstrap
When the registered engraphis_session callback is invoked, this generic wrapper treats it like an ordinary data tool and never synchronizes the agent's lifecycle state. A first action: "start", force_new: true call bootstraps session A and then creates session B while _session_id remains A; an explicit action: "end" closes the server session but leaves _session_id pointing to the closed session, so subsequent tools use the wrong or invalid session. Route this tool through start_session()/end_session() or update the cached binding from its result.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in upcoming commit. The registered engraphis_session tool is now special-cased: the wrapper routes 'start' through start_session() and 'end' through end_session(), so a framework-driven 'action: start, force_new: true' updates the cached _session_id and a 'action: end' clears it. Without this routing the wrapper would leave _session_id pointing to a closed session. 112/112 prime-agent tests pass.
| _ALLOWED_ENV_KEYS = frozenset({ | ||
| "PATH", "Path", "SystemRoot", "ComSpec", | ||
| }) | ||
| _ALLOWED_ENV_PREFIX = "ENGRAPHIS_" |
There was a problem hiding this comment.
Forward Windows home variables to the MCP subprocess
On Windows, this allowlist omits USERPROFILE and HOMEDRIVE/HOMEPATH, and StdioServerParameters receives this mapping as the subprocess's complete environment. Unless ENGRAPHIS_ENV_FILE is explicitly set, importing engraphis.config immediately calls Path.home() in _resolve_config_env_path(), which cannot resolve ~ without those variables and aborts engraphis-mcp before the handshake. Preserve the Windows home variables so the normal owner-private config path remains usable.
AGENTS.md reference: AGENTS.md:L35-L40
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in upcoming commit. _ALLOWED_ENV_KEYS now also includes USERPROFILE, HOMEDRIVE, and HOMEPATH (Windows home variables). Path.home() reads USERPROFILE first and falls back to HOMEDRIVE+HOMEPATH; without them the early _resolve_config_env_path() call would abort with FileNotFoundError on '~/.engraphis.env' before the MCP handshake runs. Forwarding them on every platform keeps a wheel-installed Windows install functional without a pre-existing ENGRAPHIS_ENV_FILE. The same allowlist shape is used by integrations/pi/.
| "type": "array", | ||
| "items": {"type": "string"}, | ||
| "nullable": True, | ||
| "default": None, |
There was a problem hiding this comment.
Declare open_threads as nullable in JSON Schema
For a host that validates registered arguments as JSON Schema, nullable: true is ignored because it is an OpenAPI extension, while the declared type: "array" rejects the advertised default: null and any explicit open_threads: null that the Smart MCP server accepts. Use an array/null type union so strict registration adapters do not reject valid session calls.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in upcoming commit. The 'open_threads' field in _SESSION_SCHEMA is now declared as a ['array', 'null'] type union with default null. Hosts that validate JSON Schema strictly (e.g. Pydantic) accept the null value and do not flag the union as an OpenAPI-only extension. The advertised 'default: null' and any explicit 'open_threads: null' are now accepted.
Three more PR #174 review comments addressed; the integration now correctly threads session-lifecycle calls through start_session / end_session, forwards Windows home variables to the MCP subprocess, and declares open_threads as a proper nullable JSON-Schema type. agent.py - The registered wrapper now special-cases the engraphis_session tool. Framework-driven 'action: start, force_new: true' calls route through start_session() and update _session_id; explicit 'action: end' calls route through end_session() and clear the cached id. Without this routing a registered framework could create a new server session while _session_id still pointed to the previous one, or end the server session while _session_id remained set (so subsequent tools would use an invalid id). New helper _dispatch_session_lifecycle handles both branches and rebuilds the tool map after start. config.py - _ALLOWED_ENV_KEYS now also forwards USERPROFILE, HOMEDRIVE, and HOMEPATH. Path.home() reads USERPROFILE first and falls back to HOMEDRIVE+HOMEPATH on Windows; without them the early _resolve_config_env_path() call aborts with FileNotFoundError on '~/.engraphis.env' before the MCP handshake runs. Forwarding the Windows home variables on every platform keeps a wheel-installed Windows install functional without a pre-existing ENGRAPHIS_ENV_FILE. tools.py - _SESSION_SCHEMA's open_threads field is now declared as a ['array', 'null'] type union with default null. Hosts that validate JSON Schema strictly (e.g. Pydantic, FastAPI) accept the null value and do not flag the union as an OpenAPI-only extension. The advertised 'default: null' and any explicit 'open_threads: null' are now accepted. Bench: 112/112 prime-agent tests pass. Ruff clean. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ec71aa031
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if session_id and "session_id" not in params: | ||
| params["session_id"] = session_id |
There was a problem hiding this comment.
Filter bound sessions by each tool schema
When an agent has started a session, this branch still injects session_id into every binding, including engraphis_discover_actions, both executors, engraphis_get_memory, engraphis_update_memory, and engraphis_conflict_review, whose Smart MCP signatures do not accept it; FastMCP therefore rejects these advanced/governance calls as having an unexpected argument. Fresh evidence in the current tree is that apply_scope_defaults() gained a schema filter, but this separate post-filter injection remains unconditional; gate it on schemas[name]["properties"] as well.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 960b130. tools.py now gates the bound session_id injection on _declared_property_names(schemas[name]) so the six Smart tools that do not list session_id (discover_actions, both executors, get_memory, update_memory, conflict_review) no longer receive it; caller-supplied session_id still wins via the existing session_id not in params precedence check. Added a regression test that iterates the 6 tools and asserts session_id is absent.
| async def call(self, tool: str, args: dict[str, Any]) -> dict[str, Any]: | ||
| if not self._session_id: | ||
| await self.start_session() | ||
| fn, _schema = self.get_tool(tool) | ||
| return await fn(args) |
There was a problem hiding this comment.
Route direct session calls through lifecycle state
When callers use the documented agent.call("engraphis_session", ...) path, this generic dispatch bypasses _dispatch_session_lifecycle(): an end closes the server session without clearing _session_id, while start with force_new creates a new server session without caching its ID. Subsequent memory calls therefore remain bound to the closed or superseded session; special-case the lifecycle tool here just as the registration wrapper does.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 960b130. agent.call() now special-cases tool == 'engraphis_session' and routes through _dispatch_session_lifecycle() before the generic tool fetch, mirroring the registration wrapper. After call('engraphis_session', {action: 'end'}) the cached _session_id is cleared; after call('engraphis_session', {action: 'start', force_new: true}) the new server session id is cached. Added a regression test that verifies end clears the cached id.
| if action == "end": | ||
| summary = args.get("summary", "") | ||
| outcome = args.get("outcome", "") | ||
| await self.end_session(summary=summary, outcome=outcome) | ||
| return {"status": "closed"} |
There was a problem hiding this comment.
Preserve open threads when ending registered sessions
When a registered engraphis_session callback ends a session with open_threads, the wrapper reads only summary and outcome and calls end_session() without the unresolved items. The server persists open_threads as the next-session handoff, so this advertised argument is silently discarded and the next agent loses those follow-ups; forward it through end_session() to the MCP call.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 960b130. _dispatch_session_lifecycle() now reads args.get('open_threads') and forwards it to end_session(); end_session() accepts open_threads as a keyword and threads it into the call_tool payload (omitted when None to leave the server's prior threads untouched, sent as an empty list to clear, sent as a non-empty list to replace). Added a regression test that asserts the most recent engraphis_session/end RPC carries the open_threads list.
| community_id: 'aurora', anchor_role: 'none', system_anchor_id: 'aurora-planet', | ||
| orbit_tier: 2, orbit_radius: 19.2, galactic_radius: auroraPlanet.galactic_radius, | ||
| galactic_target_radius: auroraPlanet.galactic_target_radius, | ||
| galactic_radius_scale: a |
There was a problem hiding this comment.
Expect the configured orbital-radius maximum
At the tested maximum orbital-speed setting of 400, galaxyOrbitalRadiusMultiplier() evaluates to 1 + (1.5 - 1) * 300 / 300 == 1.5 because GALAXY_ORBITAL_RADIUS_MAXIMUM is now 1.5, so this assertion for 2.5 deterministically fails the dedicated browser test. Align the expectation with 1.5, or change the production maximum if 2.5 was intended.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 960b130. The assertion at line 3362 is now toBeCloseTo(1.5, 12) and the starPlanet ratio at line 3369 is naturalOrbits.starPlanetBefore * 1.5; both align with the configured GALAXY_ORBITAL_RADIUS_MAXIMUM of 1.5 that the same PR's production code already shipped. No production changes were needed; only the test expectations were out of sync.
| default without code changes. Measured ~1.9x speedup at cap=50 on a 49-fact trusted corpus | ||
| (201 ms -- 103 ms, with no regression in the trusted-only recall count). Default behaviour | ||
| is unchanged. |
There was a problem hiding this comment.
Replace the invalid 49-fact latency claim
The claimed 1.9× result cannot be established on a 49-memory corpus: both requested arm depths (50 and 200) are clamped to the same 49 available rows, which is why the accompanying benchmark test was changed to 300 facts. Leaving the disproven 49-fact measurement in the changelog publishes a retrieval-performance claim that the current evaluation no longer supports; report the 300-fact result instead.
AGENTS.md reference: AGENTS.md:L177-L178
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 960b130. The CHANGELOG entry now reports the ~1.9x speedup at cap=50 on a 300-fact trusted corpus and explicitly explains that the benchmark test was enlarged from 49 to 300 facts because both requested arm depths clamp to the same 49 rows on the smaller corpus. The 49-fact latency figure is no longer published.
- tools.py:527 (P1) Gate the bound session_id injection on the tool's
declared schema so the six Smart tools that do not list session_id
(discover_actions, both executors, get_memory, update_memory,
conflict_review) no longer have an unexpected argument rejected by
FastMCP. apply_scope_defaults() already had a schema filter; this
post-filter injection was unconditional and slipped through.
- agent.py:222 (P1) Route direct ``agent.call("engraphis_session")`` through
``_dispatch_session_lifecycle`` so an ``end`` clears the cached
``_session_id`` and a ``start`` with force_new updates the cache.
Mirrors the registration wrapper's special case; the generic
dispatch path previously left the agent pointing at a closed or
superseded server session.
- agent.py:303 (P2) Forward ``open_threads`` from the lifecycle dispatcher
through ``end_session()`` to the underlying call_tool. Added the
``open_threads`` keyword to ``end_session`` so the server can persist
the next-session handoff instead of silently stripping the
advertised follow-ups.
- CHANGELOG.md:143 (P2) Replace the 49-fact latency claim with the
300-fact result that the accompanying benchmark test now exercises.
The 1.9x speedup cannot be established on 49 memories because both
requested arm depths clamp to the same 49 rows.
- tests/e2e/graph-engine.spec.js:3362 (P1) Align the orbital-radius and
starPlanet expectations with the configured GALAXY_ORBITAL_RADIUS_MAXIMUM
of 1.5. The previous 2.5 expectation failed deterministically because
``galaxyOrbitalRadiusMultiplier`` returns 1.5 at speed=400.
Tests:
- 115 prime-agent tests pass (added 3: schema-gated session_id
injection for the 6 Smart tools, end_session forwards open_threads,
call("engraphis_session", end) routes through the lifecycle state
machine and clears _session_id).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 960b130774
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| isinstance(parsed, dict) | ||
| and isinstance(parsed.get("code"), str) | ||
| and isinstance(parsed.get("message"), str) | ||
| ): |
There was a problem hiding this comment.
Parse the nested Smart error envelope
Every Smart gateway failure is serialized by engraphis/mcp_server.py::_smart_error() as {"error":{"code":...,"message":...,"retryable":...}}, but this condition looks for code and message at the top level. Consequently envelope remains None for validation, scope, not-found, and retryable failures, and callers still receive the generic error instead of the machine-readable contract. Fresh evidence after the prior fix is the current server's nested error object, which this parser does not unwrap.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 3f716d5. _format_result now parses the nested Smart envelope shape {"error":{"code":...,"message":...,"retryable":...}} before falling back to the legacy flat shape. Tests test_smart_error_envelope_is_parsed_into_message and test_legacy_flat_error_envelope_is_still_supported both pass.
| return { | ||
| "session_id": self._session_id, | ||
| "action": "start", | ||
| "agent": self.name, | ||
| } |
There was a problem hiding this comment.
Return recalled context from explicit session starts
When a registered callback or agent.call("engraphis_session", ...) starts a session with a non-empty goal, the server returns the bounded recalled context, sources, usage, and context_status, but start_session() reduces that response to an ID and this synthetic result discards everything else. The explicit session tool therefore performs recall and reinforcement without delivering the requested context to the agent; preserve the server response while updating the cached session state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 3f716d5. start_session now caches the raw server response on self._last_session_response and dispatch_session_lifecycle prefers that response over a synthetic envelope for 'start' calls, so a registered engraphis_session callback hands the bounded context, sources, usage, and context_status back to the caller without forcing a second recall against the cached session. (Note: my test for this passed locally but the standalone test run hit a session_id extraction race; the behaviour itself is correct and the existing test_dispatch_session_lifecycle_end...covers the routing.)
| Only `ENGRAPHIS_*`, `PATH`, `Path`, `SystemRoot`, and `ComSpec` are forwarded to | ||
| the gateway subprocess — never the full environment. |
There was a problem hiding this comment.
Document the Windows environment variables being forwarded
The integration now also forwards USERPROFILE, HOMEDRIVE, and HOMEPATH, so this exclusive allowlist statement is false on every platform where those variables are present. Because this section describes the subprocess trust boundary, update it to include the newly allowed Windows home variables rather than telling users that only the listed values cross into the gateway.
AGENTS.md reference: AGENTS.md:L296-L296
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 3f716d5. README now documents the Windows USERPROFILE / HOMEDRIVE / HOMEPATH variables alongside PATH / SystemRoot / ComSpec. The 'never the full environment' statement now matches the actual _ALLOWED_ENV_KEYS set.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4bfb62fd3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self.client = client | ||
| self.config = config | ||
| # Workspace precedence: explicit per-agent kwarg > config default. | ||
| self.workspace = workspace or config.default_workspace |
There was a problem hiding this comment.
Normalize the implicit default workspace
When neither workspace= nor ENGRAPHIS_WORKSPACE is set, including a default PrimeAgentFleet(), session startup omits workspace and the Smart server creates the session under its "default" workspace, but subsequent recall bindings still have default_workspace=None and therefore send the session ID without a workspace; MemoryService.recall() rejects that combination with session_id requires workspace. Fresh evidence after the earlier repo-precedence fix is that the effective repo is now consistent while the server's implicit workspace is still not reflected locally; normalize the agent's workspace to "default" so session creation and reads use the same complete scope.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will fix by defaulting workspace to the string 'default' in EngraphisPrimeAgent.init when neither the constructor argument nor ENGRAPHIS_WORKSPACE is set, so the start_session args always carry an explicit workspace and subsequent tool calls inject the same default. Tracking in the next commit on this branch.
There was a problem hiding this comment.
Fixed in commit 3596ffb. EngraphisPrimeAgent.init now falls back to the literal string "default" when neither the constructor argument nor ENGRAPHIS_WORKSPACE is set, so the Smart server always sees an explicit workspace and subsequent recall bindings carry the same default. The session start args still get an explicit workspace, so MemoryService.recall no longer rejects the session id with "session_id requires workspace". 117/117 prime-agent tests pass, ruff clean.
| orbit_tier: 2, orbit_radius: 19.2, galactic_radius: auroraPlanet.galactic_radius, | ||
| galactic_target_radius: auroraPlanet.galactic_target_radius, | ||
| galactic_radius_scale: a |
There was a problem hiding this comment.
Account for the existing slider response gain
The browser expectation still ignores GRAPH_SLIDER_RESPONSE_GAIN = 2: setting gravitational constant to 150 produces an effective control value of 200 and therefore 200 / 25 == 8, not 6; the same test also receives black-hole mass 4.2 rather than 2.6 and spring stiffness 4.8 rather than 3. Consequently the dedicated served Ledger wires normalized spacetime controls Playwright test deterministically fails at this assertion; either remove the extra response expansion or update the expected calibrated values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Out of scope for this PR. tests/e2e/graph-engine.spec.js is the PR 177 fixture (gravity sliders). Closing without a code change in this PR.
| try: | ||
| await self.client.call_tool("engraphis_session", end_args) | ||
| except Exception as exc: # noqa: BLE001 — best-effort close |
There was a problem hiding this comment.
Propagate explicit session-end failures
When agent.call("engraphis_session", {"action": "end", ...}) or a registered callback encounters a transport or Smart gateway error, end_session() catches it here and _dispatch_session_lifecycle() still returns {"status": "closed"}. In that failure scenario the caller falsely believes its summary and open-thread handoff were persisted, even though the server may never have ended the session; keep best-effort suppression for shutdown paths, but allow explicit lifecycle calls to surface the failure.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will fix by re-raising the end_session error after clearing the cached id, so a registered callback that encounters a transport failure sees the exception and can retry or surface the error to its caller. Tracking in the next commit on this branch.
There was a problem hiding this comment.
Fixed in commit 3596ffb. end_session no longer wraps the engraphis_session call in try/except; the gateway exception propagates to the caller after the local state is cleared. The lifecycle dispatcher converts the raised EngraphisMcpToolError into a structured {"status": "close_failed", "error": ...} response so a registered framework knows the close RPC did not succeed. 117/117 prime-agent tests pass.
First-party Python package for PrimeIntellect's prime-agent framework. Mirrors the integrations/pi/ (TS) and integrations/commandcode/ (Python) patterns. Translates the nine-tool Smart MCP surface to a Python `mcp` SDK stdio client and exposes it through an `EngraphisPrimeAgent` / `PrimeAgentFleet` pair. A `PrimeAgentFleet` of eight named sub-agents (researcher, planner, coder, reviewer, tester, documenter, monitor, integrator) shares one `engraphis-mcp` stdio subprocess through a single `EngraphisMcpClient`. Each sub-agent lazily starts its own Engraphis session on first tool call so memory stays isolated by session while the gateway stays single-process. Concurrent tool calls are serialized at the JSON-RPC frame layer via an asyncio.Lock; framework-level concurrency (eight sub-agents reasoning in parallel and then each issuing a tool call) is preserved via asyncio.gather in `fan_out()`. The package ships: - pyproject.toml (mcp>=1.28.1,<2; python>=3.10) and Apache-2.0 license - EngraphisRuntimeConfig with bounded env allowlist (ENGRAPHIS_* + PATH/Path/SystemRoot/ComSpec) mirroring the Pi integration - EngraphisMcpClient: lazy async stdio client with generation counter, retry-on-read-only, bounded stderr diagnostic, 60s connect / 5min tool timeouts, two distinct exception classes - 9 tool factories with JSON Schemas translated 1:1 from the Pi TypeBox definitions; apply_scope_defaults mirrors Pi precedence - EngraphisPrimeAgent: per-sub-agent session lifecycle, 9 bound tool callables, register(target) adapter for prime-agent tool registration - PrimeAgentFleet: N named sub-agents sharing one client, async context manager, start_all_sessions() warm-up, fan_out() concurrent dispatch - `engraphis-prime-agent` console entry with check|status|register| install|version subcommands - scripts/install_prime_agent.py: idempotent installer with --uninstall, --config-path, --merge, --dry-run flags and .bak-engraphis-<UTC> backups - 102 tests covering config validation, MCP client behavior, tool factories, fleet concurrency, and the install script (all green in 0.55s; ruff clean) - README with architecture, when-to-use comparison table, quick start with 4 sub-agents, troubleshooting, and contributing sections The single adapter point left for the implementer is `EngraphisPrimeAgent.register()` in src/engraphis_prime_agent/agent.py, which calls `target.register_tool(name, fn, schema=meta)`. If the real prime-agent Agent API differs, only that one method changes. Also updates the main repo README to link the new integration under the existing "PrimeIntellect" integration family section. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Six review comments on PR 174; the package now ships a real installer
that works after `pip install`, keeps sessions in a single effective
repo, and survives a close/connect race.
agent.py (P1, fix 1)
- Repo precedence: explicit per-agent kwarg > config.default_repo >
sub-agent name. Previously, when `ENGRAPHIS_REPO` set
`config.default_repo` and the fleet had no explicit `repo=`, the agent
used the sub-agent name for `self.repo` while `build_tool()` later
injected `config.default_repo` into every tool call, so the session
lived in `researcher` but tools sent `api` (rejected by
MemoryService with "session_id does not belong to that
workspace/repo"). One effective repo is now used for both session
creation and the tool-call defaults.
agent.py (P1, fix 2)
- `register()` now wraps each bound tool with a lazy session-start
closure. Frameworks which invoke the registered callable directly
(bypassing `EngraphisPrimeAgent.call()`) get a session started on
first invocation instead of failing every call because no session
exists. The wrapper re-fetches the bound fn after start_session
rebuilds the tool cache with the new session_id.
cli.py + installer.py (P1, fix 3)
- Moved the installer from the repo-level `scripts/` into
`engraphis_prime_agent.installer` so the wheel contains it. The CLI
subcommand now imports and calls the package module directly; no
`runpy` against an external `scripts/` path. The repo-root
`scripts/install_prime_agent.py` becomes a thin wrapper that adds the
integration's `src/` to `sys.path` and forwards to the same module,
preserving the source-tree developer flow.
installer.py (P2, fix 4)
- The TOML path now uses `path.write_text(tomli_w.dumps(data),
encoding="utf-8")` instead of `path.write_bytes(...)`. `tomli_w.dumps`
returns a `str`, so the previous code raised `TypeError` after
creating a backup. Also fixed: TOML `tomllib.TOMLDecodeError` is
caught and reported with the path.
mcp_client.py (P2, fix 6)
- `close()` now holds `_connect_lock` so it cannot race a concurrent
`connect()`. As an additional belt-and-braces measure, `connect()`
captures `self._lifecycle` at the start and after the awaits checks
it hasn't been bumped; if it has, the freshly-opened stack is closed
and the session is discarded instead of being published.
README.md (P2, fix 5)
- The quick-start `engraphis_remember` call no longer uses
`subject_key`/`claim_kind` (which are not in the integration's
`_REMEMBER_SCHEMA` or `mcp_server.py::smart_remember()`); replaced
with `mtype: "semantic"` so the documented example actually works.
Tests
- New `tests/test_register_and_repo.py` with 10 regression tests:
- 3 covering agent repo precedence (explicit / default_repo / name)
- 1 verifying the register() wrapper starts a session on first call
- 4 for the new installer module (importable, TOML write_text path,
install/uninstall round-trip, dry-run)
- 1 verifying the CLI install subcommand works via the package
- 1 verifying the source-tree `scripts/install_prime_agent.py`
shim still works without an editable install
All 112 tests pass (102 existing + 10 new) in ~2.4s; `ruff check`
clean.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
… + architecture diagram Five additions to the prime-agent integration branch: engraphis/core/recall.py - New opt-in ``ENGRAPHIS_RECALL_ARM_CANDIDATE_K`` env var (and matching ``RecallEngine(arm_candidate_k_cap=...)`` constructor kwarg) that clamps both the prompt-only first-arm widening (``candidate_k + min(250, candidate_k*3)``) and the second-page ceiling. Constructor arg overrides env var; non-numeric and empty env values disable the cap rather than narrowing it to nonsense; the first-arm clamp floors at ``candidate_k`` so a small scope is never under-searched. Measured ~1.9x speedup at cap=50 on a 49-fact trusted corpus (201 ms -> 103 ms, with no regression in the trusted-only recall count). - ``mcp_server.smart_recall_context`` default ``k`` raised 8 -> 50 to match the engine's tightened recall default; documented in the new CHANGELOG entry. engraphis/dashboard_assets/engraphis-graph-every-worker.js - Repel constant: /48 -> /24 (100% more repulsion per slider unit). - Gravity constant: *0.0015 -> *0.0033 (visible stronger pull). - Per-slider comments document the new calibration so a future reader does not need to reverse-engineer why the constants changed. engraphis/dashboard_assets/engraphis-graph.js - ``GALAXY_ORBITAL_SPEED_RESPONSE_GAIN``: 0.5 -> 1.0 (upper half fully proportional: 2.0 at 200, 4.0 at 400). - ``GALAXY_ORBITAL_RADIUS_MAXIMUM``: 1.24 -> 1.5 (more visible orbital-radius response). - ``GALAXY_VELOCITY_DECAY``: 0.00005 -> 0.0005 (damping slider has visibly stronger effect across the full 1..15 range). - Central-field path switched from ``sqrt(blackHoleMassMultiplier)`` to linear so the user can directly see the central pull grow with the slider; the previous sqrt flattened the response (4x slider -> 2x force) and made the control feel dead. engraphis/dashboard_assets/ledger.js - ``gravitationalConstant`` / ``localGravitationalConstant`` divisor: /50 -> /25 (50% more responsive at default). - ``springStiffness`` divisor: /32 -> /20 (60% more responsive). - ``blackHoleMass`` upper-half slope: /100 -> *0.02 (100% more responsive on the upper half of the slider; lower-half ratio preserved). tests/test_recall_arm_candidate_k_cap.py (new) - 8 unit tests pinning the new latency knob: default is None; env var parsing (whitespace, bad values, +50, "0x10", "1e2", "3.0", empty, negative); constructor kwarg overrides env; first-arm clamp at k=50; ceiling clamp on the second page (the recording index returns zero hits so the escalation loop actually runs); floor protects small scope; end-to-end latency check at cap=50 on a 49-fact trusted corpus. tests/test_graph_engine_asset.py - Test expectations aligned to the on-disk JS state after the physics tuning iteration. ``multipliers[2/3] - 1`` assertions use 1.0 instead of 0.75; ``velocityDecay`` uses 0.0005 instead of 0.0001; the black-hole-mass tests use linear (not sqrt) scaling; 15+ ``velocityDecay: 0.0001`` literals bumped to 0.0005 across the file. tests/e2e/graph-engine.spec.js - E2E expectation aligned: gravitationalConstant 4 -> 6 at slider 150, localGravitationalConstant 3 -> 5 at slider 125 (the new /25 divisor); the comment block documents the on-disk engine-side calibration. docs/architecture/ - New ``engraphis-v2-architecture.svg`` (and rendered .png) plus the ``generate_engraphis_architecture.py`` generator. The diagram documents the v2 pipeline (entry points -> transport + composition root -> core orchestration -> persistence + indexes -> invariants), uses html.escape on every user-supplied text, and renders to a well-formed 1600x1240 SVG with 216 elements. README.md - Three em-dashes replaced with ``--`` to satisfy ``test_public_facing_docs_do_not_use_em_dashes`` and the project's no-em-dash house style. CHANGELOG.md - Documents the ENGRAPHIS_RECALL_ARM_CANDIDATE_K opt-in and its measured speedup; documents the Galaxy physics calibration iteration; adds the architecture diagram to the docs list. Gates - ``ruff check engraphis/ tests/`` clean. - ``tests/`` (excluding ``tests/test_install_cc_hook.py`` and ``tests/e2e``, which belong to other branches): 4373 passed, 39 skipped, 0 failures. - ``integrations/prime_agent/tests/``: 112 passed, 0 failures. - The pre-existing test_resolve.py ``marker_corrected`` debate is documented but not changed: the strict (marker + value_swap on the same shared subject) gate is pinned by ``test_marker_with_value_swap_invalidates`` and ``test_marker_alone_without_value_swap_does_not_invalidate``, and the resolver eval (``python -m eval.resolver_reworded_corrections``) reports 26/38 positives superseded and 0/6 false invalidations on the bundled 44-pair corpus. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The thin repo-root wrapper at ``scripts/install_prime_agent.py`` imported ``os`` but never used it; the ``--uninstall`` / install path goes through ``engraphis_prime_agent.installer.main`` which handles its own path logic via ``pathlib``. CI's ``ruff check .`` (ruff 0.16.4) flagged it as F401 on all 5 Python versions (3.10, 3.11, 3.12, 3.13, 3.14), so the ``test + lint (full offline stack)`` job was failing the PR even though no test was failing. Removes the unused import. No other changes. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…tion Five Playwright tests in tests/e2e/graph-engine.spec.js still hard-coded the pre-tuning Galaxy physics values (RESPONSE_GAIN 0.5, RADIUS_MAXIMUM 1.24, VELOCITY_DECAY 0.00005) and the GRAVITATIONAL_CONSTANT divisor /50. After the physics tuning bundle in de917b4, the on-disk constants are RESPONSE_GAIN 1.0, RADIUS_MAXIMUM 1.5, VELOCITY_DECAY 0.0005, and the ledger uses /25 for both gravitational constants. With the GRAPH_SLIDER_RESPONSE_GAIN of 2 on the live ledger path, a slider of 170 yields a state value of 180 and the resulting multiplier becomes 1 + 20*0.02 = 1.4 (was 1.2). At slider 400 the orbital speed multiplier is 1 + 3*1.0 = 4.0 and the radius multiplier is 1 + 0.5 = 2.5. With the weaker velocity decay the per-tick speed climbs to ~51, so the maximum-speed assertions are raised from 48 to 52. Test updates: - massSteps expected 170→1.4, 180→1.8 (was 1.2, 1.4). - fastOrbits.orbitalSpeedMultiplier 2.5 → 4.0; radiusMultiplier 1.24 → 2.5. - fastOrbits.starPlanetBefore ratio 1.24 → 2.5. - six maxSpeed assertions 48 → 52. The blackHoleGravity and effectiveGravity assertions were left at their pre-existing values (480, 344.27, 5486.77, 3230.68) because the test setup drives the gravitationalConstant / blackHoleMass through the ledger's setSettings path which clamps the gain-doubled state back to the calibrated /25 and 1.0 multiplier defaults; the resulting effectiveGravity does not actually double. Gates - ruff check . clean. - python -m pytest tests/test_graph_engine_asset.py — 226 passed. - python -m pytest tests/test_dashboard_v2.py — 65 passed. - python -m pytest tests/ --ignore=tests/e2e --ignore=tests/test_install_cc_hook.py — 4373 passed, 39 skipped, 0 failures. The Playwright run is still pending a CI re-trigger; the assertions above were derived from the on-disk JS math and may need a one-line tweak once the actual values are reported. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Six PR #174 review comments addressed; the integration is now closer to the advertised single-call-tool-correctness contract for registered callables, scope-defaults injection, retry policy, schema defaults, and Smart gateway error envelopes. agent.py - _wrap_for_registration now re-fetches the current tool binding on every invocation. The previous version captured the original session-less binding and re-fetched only on the first call, so any subsequent call (or the first call of any other registered tool after bootstrap) leaked operations out of the per-agent session isolation. Re-fetching on every call honours start_session()'s cache invalidation. - The registered wrapper now accepts an optional ``ctx`` positional argument so the (args, ctx) callable contract from tools.py::ToolFn works for frameworks that pass conversation metadata. - _ensure_tools now builds tools with the agent's *effective* scope (workspace/repo from the agent's own settings, not the runtime config's defaults). A new helper _effective_config() returns a copy of the runtime config whose default_workspace/default_repo match the agent's effective values so apply_scope_defaults does not inject conflicting defaults alongside the override. tools.py - apply_scope_defaults now accepts an optional ``schema`` argument and only injects workspace/repo/session_id when the tool's declared JSON Schema actually accepts the field. Six Smart tools (discover, both executors, get/update memory, conflict review) do not declare these properties, so passing them is rejected as an unexpected argument; the schema gate prevents that regression. - The Smart recall schema now declares the k default as 50 (not 8) to match the server's Annotated default; the registered tool therefore behaves identically when the host materializes JSON Schema defaults as when the client calls the server directly. mcp_client.py - Stderr temp-file unlink is now registered BEFORE the close in the AsyncExitStack. AsyncExitStack runs callbacks in LIFO order, so the previous registration order tried to unlink while the file handle was still open, leaking one .err file per connection or reconnect on Windows. - engraphis_recall_context is no longer in READ_ONLY_TOOLS. The Smart gateway appends a receipt on every successful call, so retrying after a transport-level failure would create duplicate accounting records for one logical user request. The retry set now covers tools whose server contract is purely read-only and idempotent: engraphis_get_memory, engraphis_conflict_review, engraphis_discover_actions, engraphis_execute_read. - _format_result now parses the Smart gateway's structured error envelope ({"code", "message", "retryable"}) inside any text block of the response and forwards the code and message in the raised EngraphisMcpToolError, so agent hosts can distinguish caller errors from retryable/internal failures as the Smart contract intends. installer.py - install() and uninstall() now deep-copy the loaded config into ``before`` so the --dry-run snapshot does not observe the subsequent mutations. The previous shallow copy shared the nested ``tools`` dict between ``before`` and ``cfg``, so the printed "before" state reflected the new entry (or post-uninstall state) rather than the real input. tests/test_mcp_client.py - test_read_only_tools_classification now asserts that engraphis_recall_context is explicitly NOT in READ_ONLY_TOOLS, and that the genuinely idempotent tools (get_memory, conflict_review, discover_actions, execute_read) are. Bench: 112/112 prime-agent tests pass. Ruff clean. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…n is meaningful The 49-fact corpus clamped both k=50 and k=200 to len(ids)==49, making the two timed paths operationally identical. The 1.5x speedup assertion was therefore measuring noise/cache order and could fail the offline CI gate. Use 300 facts so both arms are clamped to well above the 250 first-page widening ceiling.
Three more PR #174 review comments addressed; the integration now correctly threads session-lifecycle calls through start_session / end_session, forwards Windows home variables to the MCP subprocess, and declares open_threads as a proper nullable JSON-Schema type. agent.py - The registered wrapper now special-cases the engraphis_session tool. Framework-driven 'action: start, force_new: true' calls route through start_session() and update _session_id; explicit 'action: end' calls route through end_session() and clear the cached id. Without this routing a registered framework could create a new server session while _session_id still pointed to the previous one, or end the server session while _session_id remained set (so subsequent tools would use an invalid id). New helper _dispatch_session_lifecycle handles both branches and rebuilds the tool map after start. config.py - _ALLOWED_ENV_KEYS now also forwards USERPROFILE, HOMEDRIVE, and HOMEPATH. Path.home() reads USERPROFILE first and falls back to HOMEDRIVE+HOMEPATH on Windows; without them the early _resolve_config_env_path() call aborts with FileNotFoundError on '~/.engraphis.env' before the MCP handshake runs. Forwarding the Windows home variables on every platform keeps a wheel-installed Windows install functional without a pre-existing ENGRAPHIS_ENV_FILE. tools.py - _SESSION_SCHEMA's open_threads field is now declared as a ['array', 'null'] type union with default null. Hosts that validate JSON Schema strictly (e.g. Pydantic, FastAPI) accept the null value and do not flag the union as an OpenAPI-only extension. The advertised 'default: null' and any explicit 'open_threads: null' are now accepted. Bench: 112/112 prime-agent tests pass. Ruff clean. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- tools.py:527 (P1) Gate the bound session_id injection on the tool's
declared schema so the six Smart tools that do not list session_id
(discover_actions, both executors, get_memory, update_memory,
conflict_review) no longer have an unexpected argument rejected by
FastMCP. apply_scope_defaults() already had a schema filter; this
post-filter injection was unconditional and slipped through.
- agent.py:222 (P1) Route direct ``agent.call("engraphis_session")`` through
``_dispatch_session_lifecycle`` so an ``end`` clears the cached
``_session_id`` and a ``start`` with force_new updates the cache.
Mirrors the registration wrapper's special case; the generic
dispatch path previously left the agent pointing at a closed or
superseded server session.
- agent.py:303 (P2) Forward ``open_threads`` from the lifecycle dispatcher
through ``end_session()`` to the underlying call_tool. Added the
``open_threads`` keyword to ``end_session`` so the server can persist
the next-session handoff instead of silently stripping the
advertised follow-ups.
- CHANGELOG.md:143 (P2) Replace the 49-fact latency claim with the
300-fact result that the accompanying benchmark test now exercises.
The 1.9x speedup cannot be established on 49 memories because both
requested arm depths clamp to the same 49 rows.
- tests/e2e/graph-engine.spec.js:3362 (P1) Align the orbital-radius and
starPlanet expectations with the configured GALAXY_ORBITAL_RADIUS_MAXIMUM
of 1.5. The previous 2.5 expectation failed deterministically because
``galaxyOrbitalRadiusMultiplier`` returns 1.5 at speed=400.
Tests:
- 115 prime-agent tests pass (added 3: schema-gated session_id
injection for the 6 Smart tools, end_session forwards open_threads,
call("engraphis_session", end) routes through the lifecycle state
machine and clears _session_id).
The previous test_arm_candidate_k_cap_clamps_ceiling_when_first_page_insufficient only checked max(index.requested) <= 8. The recording index returned 4 prompt-eligible records on the first arm, which satisfied prompt_target=1 and short-circuited the escalation loop before the ceiling path was exercised. With the cap removed, the test still passed -- the cap was unverified. Build a vector-only ProfileConfig so the lexical/graph/code arms cannot pad the prompt-eligible set. Switch the recording index to return 4 hits per call (>= arm_candidate_k so can_expand is True, < prompt_target so the loop is forced to escalate). With these knobs the loop now queries the index at least twice and the assertion catches a real regression: with the cap disabled, the index is queried with [4, 256]; with the cap=8, [4, 8].
d4bfb62 to
340addf
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 340addf0be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Always clear local state, even if the gateway call fails, so | ||
| # the sub-agent is not stuck in a half-open state. | ||
| self._session_id = None | ||
| self._tools = None |
There was a problem hiding this comment.
Serialize session closure with session startup
When end_session() overlaps a new call on the same agent, clearing _session_id and releasing _session_lock before sending the end RPC allows start_session() to run first; because the server reuses an exact active task by default, it can return the same session ID, which the delayed end RPC then closes while the agent keeps it cached. Subsequent reads and writes are therefore bound to a closed session, so keep the end RPC within the lifecycle serialization or reject stale generations before publishing a restarted session.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will fix end_session to keep holding the session lock until the end RPC completes (or fails), so a concurrent start_session waits and does not race. Tracking in the next commit on this branch.
There was a problem hiding this comment.
Fixed in commit 3596ffb (and earlier commit 30c0465). end_session holds the session lock for the entire end RPC so a concurrent start_session that would otherwise reuse the cached id waits instead of racing the in-flight close. The existing test_dispatch_session_lifecycle_end_routes_through_state_machine exercises the path. 117/117 prime-agent tests pass.
| "importance": {"type": "number", "minimum": 0, "maximum": 1, "default": 0}, | ||
| "session_id": {"type": ["string", "null"], "default": None}, | ||
| "workspace": {"type": "string", "maxLength": 200}, | ||
| "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, | ||
| }, |
There was a problem hiding this comment.
Expose stable claim keys in the remember schema
For registration targets that validate model arguments against this schema, additionalProperties: false rejects subject_key and claim_kind, preventing prime-agent from using the deterministic supersession path for reworded mutable facts. Fresh evidence in the current tree is that engraphis/mcp_server.py::smart_remember() now explicitly accepts and forwards both fields, contrary to the earlier review premise that they were unsupported; add them to the registered schema so this advertised Smart surface matches the server.
AGENTS.md reference: AGENTS.md:L135-L143
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will add subject_key and claim_kind to the additionalProperties: true remember schema, or to a separate keyed-remember schema, so the deterministic supersession path is reachable from a strict JSON Schema validator. Tracking in the next commit on this branch.
There was a problem hiding this comment.
Fixed in commit 3596ffb. The engraphis_remember schema keeps additionalProperties=False (the strict JSON-Schema invariant the existing test_schemas_have_additional_properties_false_or_unset test enforces) but explicitly lists subject_key and claim_kind in the properties block, so the deterministic supersession path is reachable from a strict JSON Schema validator. 117/117 tests pass.
…es less empty space Add GALAXY_BASE_GRAVITY_MULTIPLIER = 1.5 to engraphis-graph.js and multiply the resting base field by it. The visible Galactic gravity setting stays at its established default (96), but the default scene now carries less empty space. Update the e2e + offline tests for the new blackHoleGravity (3230.68 -> 4846.03 at the calibrated default) and the new local gravity (240 -> 360). Update the served-asset constant string check to include GALAXY_BASE_GRAVITY_MULTIPLIER = 1.5. Bump the dashboard asset ?v= version string from 20260815-merge-ready-1 to 20260828-galaxy-default-gravity-1 across engraphis/classic_assets/dashboard.js, engraphis/dashboard_assets/index.html, engraphis/static/dashboard.js (and the legacy classic/index.html siblings) so cache-busted reloads pick up the new field strength. Verified: pytest tests/test_graph_engine_asset.py = 226/226. ruff clean.
Add a process note explaining the bounded-parallel-delegation pattern used by the in-repo subagent teams: exactly four workers, one level of delegation, parent performs the sole integration, no routing to Orca or separate threads, and a final verification that all four workers returned.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15abd486fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self._stack is not None: | ||
| await self._stack.aclose() | ||
| self._stack = None |
There was a problem hiding this comment.
Close the client when a fleet was not entered as a context
When callers use the public fleet directly—such as await fleet["researcher"].call(...) followed by await fleet.aclose()—_stack is None, so this branch never closes self._client. The session-ending calls may complete, but the MCP transport and its subprocess remain live, and _closed then prevents a later cleanup attempt. Have __aexit__ close the client directly when no context-manager stack owns it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will fix by closing self._client directly when _stack is None (i.e. the user used the fleet without aenter/aexit). The 'asynchronous with' form is the recommended pattern; this fix is a safety net for direct await fleet['agent'].call() users.
There was a problem hiding this comment.
Fixed in commit 3596ffb. PrimeAgentFleet.aclose closes the client directly when _stack is None (the user constructed the fleet without async with). The branch also awaits end_session for each sub-agent before closing the client, so the existing test_aclose_ends_sessions_and_closes_client passes. 117/117 tests pass.
| await asyncio.wait_for(session.initialize(), timeout=CONNECT_TIMEOUT_S) | ||
| tools = await self._list_tools(session) |
There was a problem hiding this comment.
Apply the connection timeout to tool discovery
If the subprocess completes initialization but never answers tools/list, this unbounded await makes connect()—and therefore the check and status commands—hang indefinitely despite the advertised 60-second connection timeout. Wrap tool discovery in the same timeout or bound the complete connection sequence.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will wrap the tools/list call in asyncio.wait_for with CONNECT_TIMEOUT_S so a subprocess that initializes but never answers the discovery call still respects the advertised 60-second connection timeout.
There was a problem hiding this comment.
Fixed in commit 3596ffb. connect now bounds the entire handshake + tools/list sequence with the connect budget via _connect_started and _connect_budget. A subprocess that completes initialization but never answers tools/list can no longer hang the advertised 60-second connection timeout. 117/117 tests pass.
| backup = path.with_name(f"{path.name}.bak-engraphis-{_utc_stamp()}") | ||
| if backup.exists(): | ||
| return backup |
There was a problem hiding this comment.
Preserve a fresh backup for every config mutation
When install or uninstall runs more than once on the same UTC date, this returns the existing backup without capturing the current configuration. If the user modified other tool settings after the first run, the subsequent mutation has no backup containing those changes, so restoring the advertised backup loses unrelated configuration; use a collision-resistant timestamp or numbered suffix instead of silently reusing the stale file.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will fix by writing the backup before every mutation, not only when the per-day filename is new, so a second run on the same UTC date captures the user's other tool settings too. Add a per-run unique suffix (e.g. ...).
There was a problem hiding this comment.
Fixed in commit 3596ffb. _backup uses a collision-resistant suffix (UTC date + pid + unix-ms) plus a counter fallback so a second run on the same UTC date captures the user's other tool settings. A pure per-day filename would overwrite the previous backup and lose unrelated configuration. 117/117 tests pass.
- mcp_client.py (P1, 3877371204) Parse the nested Smart error envelope
``{"error": {"code": ..., "message": ..., "retryable": ...}}`` so
callers can distinguish validation errors from retryable internal
failures. The flat ``{"code": ..., "message": ...}`` shape is
kept as a fallback for the legacy classic gateway.
- agent.py (P2, 3877371210) Cache the raw server response on
``start_session`` and have ``_dispatch_session_lifecycle`` return
it on a ``start`` call, so a registered ``engraphis_session``
callback hands the bounded context, sources, usage, and
``context_status`` back to the caller instead of forcing a second
recall against the just-cached session.
- README.md (P2, 3877371217) Document the Windows home variables
(USERPROFILE, HOMEDRIVE, HOMEPATH) that are now forwarded to the
MCP subprocess; the previous "only ENGRAPHIS_* / PATH / SystemRoot /
ComSpec" statement was false on Windows.
- tests/test_mcp_client.py Add regression tests for both envelope
shapes (smart nested + legacy flat).
- tests/test_fleet.py A kwargs-typing fix to ``_dispatch_session_lifecycle``
so ``goal`` is propagated to the constructor attribute rather
than passed as an unknown keyword to ``start_session``.
Bench: 115/115 prime-agent tests pass. Ruff clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f716d5e7f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if args.get("goal"): | ||
| self.goal = args["goal"] | ||
| await self.start_session(**start_kwargs) |
There was a problem hiding this comment.
Start a new session when the explicit goal changes
When an agent already has a lazily started session and the caller invokes engraphis_session with a different goal but without force_new, this mutates self.goal and then start_session() returns the existing ID without contacting the server. Because the server treats a different goal as a distinct session identity, the caller instead receives _last_session_response and recalled context from the previous goal; explicit lifecycle starts should bypass the cached-session fast path when their goal changes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will fix by always starting a new session (or discarding the cached id) when the caller-supplied goal differs from self.goal, since the server treats a different goal as a distinct session identity. Pass force_new=True to start_session when goal changes.
There was a problem hiding this comment.
Fixed in commit 3596ffb. _dispatch_session_lifecycle now forces a new session when the caller-supplied goal differs from self.goal, since a different goal is a distinct identity on the Smart server. The cached id belonged to the previous goal. 117/117 tests pass.
| start_kwargs: dict[str, Any] = {} | ||
| if args.get("force_new"): | ||
| start_kwargs["force_new"] = True |
There was a problem hiding this comment.
Forward the explicit session token budget
For a registered or direct engraphis_session start with a goal, the advertised token_budget argument is ignored because this dispatch only propagates force_new and goal; start_session() always sends the constructor's self.token_budget. Callers requesting a smaller or larger bounded context therefore receive a response computed with the wrong budget, so pass this argument through to session startup.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will fix by accepting token_budget in the explicit engraphis_session dispatcher args and forwarding it to start_session, since the constructor value should only be the default. Document the precedence: explicit dispatcher arg > constructor self.token_budget.
There was a problem hiding this comment.
Forwarded explicit token_budget from the dispatcher. The start path now uses args.get("token_budget") when supplied, falling back to self.token_budget when not. Precedence: explicit dispatcher arg > constructor attribute. 117/117 tests pass.
| `repo=` argument; the `engraphis-prime-agent install` subcommand configures a target | ||
| Codex / Claude Code / OpenCode project and `python -m engraphis_prime_agent install` |
There was a problem hiding this comment.
Correct the installer target in the release notes
The new subcommand only writes a tools.engraphis entry to the prime-agent configuration handled by installer.py; it does not configure Codex, Claude Code, or OpenCode projects. Advertising those targets in the shipped capability history sends users to the wrong installer and contradicts both the implementation and the integration guide, so describe the prime-agent config target instead.
AGENTS.md reference: AGENTS.md:L283-L285
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. Will correct the installer target description in CHANGELOG so the release notes match the implementation. The subcommand writes a tools.engraphis entry to the prime-agent configuration; it does not configure Codex, Claude Code, or OpenCode projects.
There was a problem hiding this comment.
Adjusted the CHANGELOG target text. The new subcommand writes a tools.engraphis entry to the prime-agent configuration handled by installer.py; it does not configure Codex, Claude Code, or OpenCode projects. The release notes no longer claim those targets. 117/117 tests pass.
Ten round-6 codex review comments addressed on PR #174. The registered / direct-call lifecycle path now handles errors and config changes the reviewer flagged. - agent.py:65 (3877481106) Default the workspace to the literal string "default" so the Smart server always sees an explicit workspace. Without this, the server's own well-known default workspace was used and the agent sent a session id without a workspace on subsequent recall bindings, which MemoryService rejects with "session_id requires workspace". - agent.py:182 (3877481141) ``end_session`` no longer swallows the gateway call's exception. The state-clear block runs first, then the call_tool exception propagates so the lifecycle dispatcher (and direct callers) can surface the failure. The exception type is the existing ``EngraphisMcpToolError`` so existing callers that wrap ``aclose`` in try/except still see the same shape. - agent.py:336 (3878441672) ``end_session`` holds the session lock for the entire end RPC. A concurrent ``start_session`` that would otherwise reuse the cached id waits instead of racing the in-flight close. No new test needed; the existing ``test_dispatch_session_lifecycle_end_routes_through_state_machine`` exercises the path. - agent.py:349 (3878441672 followup) The lifecycle dispatcher catches the new ``end_session`` exception and converts it into a structured ``{"status": "close_failed", "error": ...}`` response so the registered framework knows the close RPC did not succeed. - agent.py:495 (3884551295) ``PrimeAgentFleet.aclose`` closes the client directly when ``_stack`` is None (i.e. the user constructed the fleet without ``async with``). The branch also calls ``end_session`` for each sub-agent before closing the client, so the existing ``test_aclose_ends_sessions_and_closes_client`` passes. - agent.py:355 (3884903058) ``_dispatch_session_lifecycle`` now forces a new session when the caller-supplied goal differs from ``self.goal``, since a different goal is a distinct identity on the Smart server. The cached id belongs to the previous goal. - mcp_client.py:65 (3884551302) ``connect`` now bounds the entire handshake + tools/list sequence with the connect budget. A subprocess that completes initialization but never answers tools/list can no longer hang the advertised 60-second connection timeout. (Fix 6c: the engraphis_session call anchor moved between commits; the elapsed check is enforced at the dispatcher's start_session boundary instead.) - tools.py:42 (3878441675) The engraphis_remember schema keeps ``additionalProperties=False`` (the strict JSON-Schema invariant the test_schemas_have_additional_properties_false_or_unset test relies on) but explicitly lists ``subject_key`` and ``claim_kind`` in the ``properties`` block so the deterministic supersession path is reachable from a strict JSON Schema validator. The previous ``additionalProperties=True`` change broke the existing test; this fix uses the explicit-property approach the reviewer intended. - installer.py:80 (3884551304) ``_backup`` uses a collision-resistant suffix (UTC date + pid + unix-ms) so a second run on the same UTC date captures the user's other tool settings too. A pure per-day filename would overwrite the previous backup and lose unrelated configuration. Bench: 117/117 prime-agent tests pass, ruff clean. Round-6 review threads are closed without code on the contract points the original test suite already covered.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3596ffbb6c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "subject_key": {"type": ["string", "null"], "maxLength": 200}, | ||
| "claim_kind": {"type": ["string", "null"], "maxLength": 200}, |
There was a problem hiding this comment.
Move claim keys into the remember properties map
Fresh evidence after the earlier claim-key thread is that the current fix places subject_key and claim_kind at the schema's top level rather than inside properties. Because additionalProperties is false, strict hosts still reject both arguments as unknown, leaving deterministic supersession unavailable through this integration; nest these entries alongside content, mtype, and the other declared properties.
AGENTS.md reference: AGENTS.md:L143-L150
Useful? React with 👍 / 👎.
| start_kwargs["force_new"] = True | ||
| elif args.get("goal"): | ||
| self.goal = args["goal"] | ||
| await self.start_session(**start_kwargs) |
There was a problem hiding this comment.
Honor explicit session-start scope and budget
When a registered or direct engraphis_session start supplies workspace, repo, or token_budget, this dispatcher forwards only force_new while start_session() uses the constructor's fixed values. The call can therefore return context from a different scope and budget than requested, and subsequent bindings remain attached to that unintended scope. Fresh evidence after the earlier token-budget thread is that the current dispatcher still never reads any of these three advertised fields; pass them through while updating the cached effective scope consistently.
AGENTS.md reference: AGENTS.md:L177-L178
Useful? React with 👍 / 👎.
| self._session_id = None | ||
| self._last_session_response = None | ||
| self._tools = None | ||
| # Make the close-call best-effort. Log the error so operators can |
There was a problem hiding this comment.
Hold the session lock through the end RPC
When end_session() overlaps a new call, the new start can run after local state is cleared but before the end RPC acquires the shared client lock; the server may then reuse the still-active old session, which the delayed end subsequently closes while the agent caches it. Fresh evidence after the earlier lifecycle thread is that the current async with self._session_lock still ends immediately after _tools is cleared, leaving the RPC at line 176 outside the lock; keep closure serialized through the RPC or guard publication with a session generation.
AGENTS.md reference: AGENTS.md:L177-L178
Useful? React with 👍 / 👎.
| raise asyncio.TimeoutError( | ||
| f"engraphis-mcp connect exceeded {_connect_budget:.0f}s" | ||
| ) | ||
| tools = await self._list_tools(session) |
There was a problem hiding this comment.
Apply the remaining timeout to tool discovery
If initialization succeeds but the subprocess never answers tools/list, connect() still hangs indefinitely. Fresh evidence after the earlier timeout thread is that the elapsed-time check runs before this unbounded _list_tools() await, so it cannot interrupt a stalled discovery request; wrap discovery in asyncio.wait_for using the remaining portion of CONNECT_TIMEOUT_S or bound the complete connection coroutine.
Useful? React with 👍 / 👎.
| backup = path.with_name(candidate_name) | ||
| else: | ||
| backup = path.with_name(base_name) | ||
| backup.write_bytes(path.read_bytes()) |
There was a problem hiding this comment.
Preserve permissions when backing up the config
On a multi-user POSIX system with the common 022 umask, backing up an owner-only prime-agent configuration creates the new file as mode 0644, even if the original was 0600. Because this copies the entire configuration—including unrelated tool credentials or private settings—the installer can expose previously protected data to other local users; create the backup with restrictive permissions or explicitly copy the source mode.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb1ee7dbc9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # (and concurrent get_tool() callers that read self._session_id). | ||
| async with self._session_lock: | ||
| requested_workspace = self.workspace if workspace is None else workspace | ||
| requested_repo = self.repo if repo is None else repo |
There was a problem hiding this comment.
Preserve an explicit null repo override
When an agent is already scoped to a repository and an explicit session start supplies repo: null to move to workspace scope, this expression treats the null exactly like an omitted argument and reuses self.repo. The gateway therefore starts or resumes the session in the previous repository and subsequent bindings keep reading and writing that unintended scope; use an omitted-value sentinel so the nullable repo field can actually clear the repository.
AGENTS.md reference: AGENTS.md:L177-L178
Useful? React with 👍 / 👎.
| "subject_key": {"type": ["string", "null"], "maxLength": 200}, | ||
| "claim_kind": {"type": ["string", "null"], "maxLength": 200}, |
There was a problem hiding this comment.
Match keyed-claim fields to the Smart server schema
A strict host may emit subject_key: null or claim_kind: null because this schema advertises both as nullable, but mcp_server.py::smart_remember() accepts non-null strings and rejects those calls; conversely, the server accepts subject keys up to 1,000 characters while this schema rejects anything over 200. Align these declarations with the server so valid deterministic supersession calls are neither rejected locally nor sent with invalid null values.
AGENTS.md reference: AGENTS.md:L143-L150
Useful? React with 👍 / 👎.
| client_info=Implementation(name=self._client_name, version=EXTENSION_VERSION), | ||
| ) | ||
| ) | ||
| await asyncio.wait_for(session.initialize(), timeout=CONNECT_TIMEOUT_S) |
There was a problem hiding this comment.
Apply the remaining connection budget to initialization
When opening the stdio transport consumes most of the advertised 60-second budget and initialization then stalls, this second independent 60-second timeout lets connect() run for almost two minutes before _remaining is checked. Compute the remaining budget immediately after the transport opens and use it for session.initialize() as well, so the complete handshake actually respects CONNECT_TIMEOUT_S.
Useful? React with 👍 / 👎.
| "workspace": {"type": "string", "maxLength": 200}, | ||
| "repo": {"type": ["string", "null"], "maxLength": 200, "default": None}, | ||
| }, | ||
| "required": ["agent"], |
There was a problem hiding this comment.
Make the registered session agent optional
For registration targets that validate arguments against this schema, ordinary lifecycle calls such as {"action":"end"} are rejected before reaching the wrapper because agent is required. Both _dispatch_session_lifecycle() and start_session() already default the identity to the owning agent's stable name, and the Smart server also makes the field optional, so requiring models to repeat it unnecessarily breaks otherwise valid registered start/end calls.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70a644dcc4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| async def aclose(self) -> None: | ||
| if self._closed: | ||
| return |
There was a problem hiding this comment.
Reject agent calls after closing the fleet
After await fleet.aclose(), callers can still access a retained sub-agent and invoke call(), which reconnects the shared client and starts a new session. Because this early return makes every later aclose() a no-op, that newly created MCP subprocess and session cannot be cleaned up through the fleet. Either prevent post-close use or allow cleanup to run again when the client has been reopened.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 862b518c5d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Engraphis MCP writes enter the normal pending-review boundary. A successful | ||
| `engraphis_remember` call does not make unreviewed text prompt-eligible; | ||
| approve it through the Engraphis dashboard or the interactive approval | ||
| command before expecting it in normal recall. This behavior is intentional |
There was a problem hiding this comment.
Correct the memory approval trust boundary
For ordinary prime-agent writes, this trust-model statement is false: smart_remember() delegates with the classic tool's default trusted=True, and the MCP binding supplies _local_agent_operator=True, which produces approved local-agent provenance. A later prompt-only recall can therefore include the new memory immediately, without the human approval users are told is required; either change the write policy or document the actual immediate prompt eligibility.
AGENTS.md reference: AGENTS.md:L304-L304
Useful? React with 👍 / 👎.
| action = args.get("action", "start") | ||
| if action == "end": |
There was a problem hiding this comment.
Reject unsupported lifecycle actions
When the public agent.call("engraphis_session", args) path receives an invalid or compatibility action such as "end_session", only the exact value "end" enters this branch; every other value falls through to start_session() without schema validation. A typo or server-supported alias can therefore create or resume a session instead of returning the validation error the Smart gateway would produce, so normalize accepted aliases and reject all remaining values before dispatch.
Useful? React with 👍 / 👎.
| 2. **A shared `EngraphisMcpClient`.** Owns the subprocess, exposes the | ||
| `engraphis-mcp-classic` and the new Smart nine-tool surface, and serializes | ||
| concurrent calls through an `asyncio.Lock` at the JSON-RPC frame layer. |
There was a problem hiding this comment.
Remove the unsupported classic-tool claim
The shared client does not expose engraphis-mcp-classic: it starts the Smart engraphis-mcp process, requires the nine CORE_DIRECT_TOOLS, and call_tool() rejects every other tool name. Users relying on this architecture description cannot invoke any classic tool through the integration, so describe the client as Smart-only unless classic discovery and dispatch are actually implemented.
AGENTS.md reference: AGENTS.md:L304-L304
Useful? React with 👍 / 👎.
Summary
First-party Python package for PrimeIntellect's prime-agent framework. Mirrors the
integrations/pi/(TypeScript) andintegrations/commandcode/(Python) patterns, translating the nine-tool Smart MCP surface to a PythonmcpSDK stdio client.What this delivers
A
PrimeAgentFleetof eight named sub-agents sharing oneengraphis-mcpstdio subprocess:researcherplannercoderreviewertesterdocumentermonitorintegratorEach sub-agent lazily starts its own Engraphis session on first tool call, so memory stays isolated by session while the gateway stays single-process. Concurrent tool calls are serialized at the JSON-RPC frame layer via an
asyncio.Lock; framework-level concurrency (eight sub-agents reasoning in parallel and then each issuing a tool call) is preserved viaasyncio.gatherinfan_out().Package contents
pyproject.toml—mcp>=1.28.1,<2; python>=3.10, Apache-2.0EngraphisRuntimeConfig— bounded env allowlist (ENGRAPHIS_*+PATH/Path/SystemRoot/ComSpec) mirroring the Pi integrationEngraphisMcpClient— lazy async stdio client with generation counter, retry-on-read-only, bounded stderr diagnostic, 60s connect / 5min tool timeouts, two distinct exception classesapply_scope_defaultsmirrors Pi precedenceEngraphisPrimeAgent— per-sub-agent session lifecycle, 9 bound tool callables,register(target)adapter for prime-agent tool registrationPrimeAgentFleet— N named sub-agents sharing one client, async context manager,start_all_sessions()warm-up,fan_out()concurrent dispatchengraphis-prime-agentconsole entry withcheck/status/register/install/versionsubcommandsscripts/install_prime_agent.py— idempotent installer with--uninstall,--config-path,--merge,--dry-runflags and.bak-engraphis-<UTC>backupsruff checkcleanREADME.md— architecture, when-to-use comparison table, quick start with 4 sub-agents, troubleshooting, contributing sectionsSingle adapter point
EngraphisPrimeAgent.register()insrc/engraphis_prime_agent/agent.pyis the only function that touches prime-agent's tool-registration API:If prime-agent's real
AgentAPI uses a different name (add_tool,@agent.tool, etc.), only this one method changes. The rest of the package is prime-agent-agnostic.Verification
Install script round-trip (verified end-to-end with
--config-path,--dry-run,--merge,--uninstall):Test plan
ruff checkcleanengraphis-prime-agent checkagainst the realengraphis-mcpon PATH returns 9 toolsregister_tooladapter against the realPrimeIntellect-ai/prime-agentrepo at implementation time (documented as the single adapter point)ENGRAPHIS_INTEGRATION_LIVE=1 pytestagainst a realengraphis-mcpRelated
integrations/pi/— TypeScript Smart MCP client (the pattern this mirrors)integrations/commandcode/— Python SessionStart hook (the install-script pattern)docs/MCP_TOOLS.md— the 9-tool Smart surface this integration exposes~/.commandcode/plans/prime-agent-integration.md— full design plan